mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Update 2018 client support with registration defaults and WebSocket handling
This commit is contained in:
@@ -72,11 +72,12 @@ public class Player2018
|
||||
[JsonPropertyName("Level")]
|
||||
public int Level { get; set; } = 1;
|
||||
|
||||
// Obfuscated enum in the dump; not a gate (the client just stores it). 3 is
|
||||
// the "fully registered" value used elsewhere; verify against a live client
|
||||
// if the signup flow re-appears for an existing account.
|
||||
// Registration step enum (ProfileSelectionManager.NFCBDCFLBOC):
|
||||
// invalid=-1, video=0, birthday=1, email=2, COMPLETE=10. Anything < 10
|
||||
// makes the client run the signup step flow (it got "stuck on birthday").
|
||||
// 10 = fully registered → client goes straight in-game.
|
||||
[JsonPropertyName("RegistrationStatus")]
|
||||
public int RegistrationStatus { get; set; } = 3;
|
||||
public int RegistrationStatus { get; set; } = 10;
|
||||
|
||||
[JsonPropertyName("Developer")]
|
||||
public bool Developer { get; set; }
|
||||
|
||||
@@ -98,11 +98,17 @@ public class PhotonConfig
|
||||
|
||||
public class MatchmakingParams
|
||||
{
|
||||
[JsonPropertyName("MaxPlayersPerRoom")]
|
||||
public int MaxPlayersPerRoom { get; set; } = 8;
|
||||
// 2018 RecNet.LOIBFONMHMF.Deserialize reads exactly TWO required float keys
|
||||
// via Util.GetKey<Single> (verified via dnlib). It's fetched as an optional
|
||||
// object (GetObjectKey) inside config/v2, but once PRESENT the client
|
||||
// deserializes it — wrong/missing keys throw KeyNotFoundException →
|
||||
// "Received malformed RecNet response" → "Failed to connect to RecNet" at
|
||||
// boot. The 2020 MaxPlayersPerRoom/MinPlayersToStart keys are NOT read here.
|
||||
[JsonPropertyName("PreferFullRoomsFrequency")]
|
||||
public float PreferFullRoomsFrequency { get; set; } = 0.5f;
|
||||
|
||||
[JsonPropertyName("MinPlayersToStart")]
|
||||
public int MinPlayersToStart { get; set; } = 1;
|
||||
[JsonPropertyName("PreferEmptyRoomsFrequency")]
|
||||
public float PreferEmptyRoomsFrequency { get; set; } = 0.5f;
|
||||
}
|
||||
|
||||
public class LevelProgressionEntry
|
||||
|
||||
@@ -1,29 +1,65 @@
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DorkNet.Server.Services;
|
||||
|
||||
namespace DorkNet.Server.Controllers.API.Notification.V2;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy raw-WebSocket notification endpoint. Replaced by the SignalR
|
||||
/// hub at <c>notify.{apex}/hub/v1</c>. Returns 410 Gone for any client
|
||||
/// that still tries the old path.
|
||||
/// api/notification/v2 — the JUNE-2018 client opens a raw WebSocket here for
|
||||
/// push notifications (RecNet.cs:48895, RecNet.IJADBHMBMPG wraps a
|
||||
/// BestHTTP.WebSocket with open/message(string)/error events). The 2020 client
|
||||
/// replaced this with the SignalR hub at notify.{apex}/hub/v1.
|
||||
///
|
||||
/// This branch targets 2018, so we ACCEPT the WebSocket and hold it open. We
|
||||
/// don't push anything yet (server-driven notifications are a later feature) —
|
||||
/// an open, quiet socket keeps the client's notification subsystem "connected"
|
||||
/// instead of erroring/retry-looping on the old 410 Gone. Incoming frames
|
||||
/// (subscribe/auth) are drained and ignored.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]/v2")]
|
||||
public class NotificationController(DomainConfig domain) : ControllerBase
|
||||
public class NotificationController(
|
||||
DomainConfig domain,
|
||||
ILogger<NotificationController> log) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult Connect() => StatusCode(StatusCodes.Status410Gone, new
|
||||
public async Task Connect()
|
||||
{
|
||||
error = "use_signalr_hub",
|
||||
hint = $"connect to {domain.Sub("notify")}/hub/v1 instead",
|
||||
});
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
// Non-WS probe (or a curl check) — return an empty notification
|
||||
// list rather than 410 so HTTP callers don't treat it as an error.
|
||||
Response.StatusCode = StatusCodes.Status200OK;
|
||||
Response.ContentType = "application/json";
|
||||
await Response.WriteAsync("[]");
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>GET <c>api/notification/hub/v1</c> — returns the
|
||||
/// SignalR hub URL the client should open. Without this the
|
||||
/// watch falls back to its own default and may try the wrong
|
||||
/// scheme. URL is built off the configured deployment apex
|
||||
/// (DORKNET_DOMAIN).</summary>
|
||||
using var ws = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
log.LogInformation("[notify] 2018 client opened notification WebSocket");
|
||||
var buf = new byte[4096];
|
||||
try
|
||||
{
|
||||
while (ws.State == WebSocketState.Open)
|
||||
{
|
||||
var result = await ws.ReceiveAsync(
|
||||
new ArraySegment<byte>(buf), HttpContext.RequestAborted);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure,
|
||||
"bye", HttpContext.RequestAborted);
|
||||
break;
|
||||
}
|
||||
// Drain & ignore subscribe/auth frames — no server push yet.
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { /* client navigated away */ }
|
||||
catch (WebSocketException) { /* abrupt disconnect — normal */ }
|
||||
log.LogDebug("[notify] notification WebSocket closed");
|
||||
}
|
||||
|
||||
/// <summary>GET <c>api/notification/hub/v1</c> — 2020 SignalR hub pointer
|
||||
/// (unused by the 2018 client; kept for any 2019/2020 caller).</summary>
|
||||
[HttpGet("/api/notification/hub/v1")]
|
||||
public IActionResult HubInfo() => Ok(new
|
||||
{
|
||||
|
||||
@@ -166,10 +166,15 @@ public class PlatformLoginV1Controller(
|
||||
// account-selection list. Returns a BARE JSON ARRAY of Player2018 objects.
|
||||
[HttpPost("getcachedlogins")]
|
||||
[HttpGet("getcachedlogins")]
|
||||
public async Task<IActionResult> GetCachedLogins(
|
||||
[FromQuery] int Platform = 0, [FromQuery] string? PlatformId = null)
|
||||
public async Task<IActionResult> GetCachedLogins()
|
||||
{
|
||||
var players = await playerService.GetCachedLoginsAsync(Platform, PlatformId);
|
||||
// The 2018 client sends Platform/PlatformId in the FORM BODY
|
||||
// (application/x-www-form-urlencoded), not the query string — so read
|
||||
// from form/query/json. Reading [FromQuery] only returned [] (empty
|
||||
// account grid + broke +profile auto-login). Verified from the wire:
|
||||
// req=Platform=0&PlatformId=76561198969189675.
|
||||
var p = await ReadParamsAsync();
|
||||
var players = await playerService.GetCachedLoginsAsync(p.GetInt("Platform"), p.Get("PlatformId"));
|
||||
return Ok(players.Select(ToPlayer2018).ToList());
|
||||
}
|
||||
|
||||
|
||||
@@ -886,6 +886,11 @@ using (var scope = app.Services.CreateScope())
|
||||
// that's already there (so spent-down accounts aren't refilled).
|
||||
await playerService.BackfillStarterWalletsAsync();
|
||||
|
||||
// 2018 client: complete any account missing birthday/email/password so the
|
||||
// (screen-mode-broken) signup step flow never appears. See
|
||||
// PlayerService.ApplyRegistrationDefaults.
|
||||
await playerService.BackfillAccountCompletionAsync();
|
||||
|
||||
// Backfill the per-player dorm room + DormStateEntity for any
|
||||
// legacy account that signed in via a path that didn't run
|
||||
// EnsurePersonalDormAsync (early-bird signups, manual SQL
|
||||
|
||||
@@ -517,6 +517,7 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger<Playe
|
||||
HairColor = StarterHairColor,
|
||||
SkinColor = StarterSkinColor,
|
||||
};
|
||||
ApplyRegistrationDefaults(player);
|
||||
|
||||
db.Players.Add(player);
|
||||
await db.SaveChangesAsync();
|
||||
@@ -596,6 +597,7 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger<Playe
|
||||
HairColor = StarterHairColor,
|
||||
SkinColor = StarterSkinColor,
|
||||
};
|
||||
ApplyRegistrationDefaults(player);
|
||||
db.Players.Add(player);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
@@ -629,6 +631,43 @@ public class PlayerService(DorkNetDbContext db, RoomService rooms, ILogger<Playe
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2018 client runs a signup step-flow (video→birthday→email→complete) for
|
||||
// accounts missing these — and its screen-mode UI is unusable, so accounts
|
||||
// are born COMPLETE: an adult birthday, a synthetic email, and a default
|
||||
// password ("dorknet"). RegistrationStatus is reported as complete(10) in
|
||||
// the Player2018 wire shape.
|
||||
private static readonly DateTime DefaultBirthday = new(1990, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly string DefaultPasswordHash = BCrypt.Net.BCrypt.HashPassword("dorknet", 11);
|
||||
|
||||
private static void ApplyRegistrationDefaults(PlayerEntity p)
|
||||
{
|
||||
p.Birthday ??= DefaultBirthday;
|
||||
if (string.IsNullOrEmpty(p.Email))
|
||||
p.Email = $"{(string.IsNullOrEmpty(p.Username) ? "player" : p.Username).ToLowerInvariant()}@dorknet.local";
|
||||
if (string.IsNullOrEmpty(p.PasswordHash)) p.PasswordHash = DefaultPasswordHash;
|
||||
// 2018 SCREEN MODE is a developer-gated feature: SessionManager.BIAOMKDOLDL
|
||||
// (which keeps screen mode enabled post-login) = account.Developer
|
||||
// (RecNet EEBPLECPEGD.Deserialize "Developer" key). Without it the client
|
||||
// demands a headset (vr_device_required) after login. Grant it so the
|
||||
// screen-mode (non-VR) client is playable on this local-test branch.
|
||||
p.IsDeveloper = true;
|
||||
}
|
||||
|
||||
/// <summary>Complete any account missing birthday/email/password so the 2018
|
||||
/// client never enters its (broken-in-screen-mode) signup step flow. Runs at
|
||||
/// startup like the other backfills.</summary>
|
||||
public async Task BackfillAccountCompletionAsync()
|
||||
{
|
||||
var incomplete = await db.Players
|
||||
.Where(p => p.Birthday == null || p.Email == null || p.PasswordHash == null || !p.IsDeveloper)
|
||||
.ToListAsync();
|
||||
if (incomplete.Count == 0) return;
|
||||
foreach (var p in incomplete) ApplyRegistrationDefaults(p);
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation(
|
||||
"[backfill] completed registration (birthday/email/password) for {N} account(s)", incomplete.Count);
|
||||
}
|
||||
|
||||
public async Task<PlayerEntity?> GetByIdAsync(long id) =>
|
||||
await db.Players.Include(p => p.Avatar).FirstOrDefaultAsync(p => p.Id == id);
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Jwt": {
|
||||
"Secret": "dorknet-local-dev-secret-please-change-0123456789abcdef"
|
||||
},
|
||||
"Domain": {
|
||||
"Apex": "localhost",
|
||||
"Scheme": "http",
|
||||
"SingleOriginBaseUrl": "http://127.0.0.1:5005"
|
||||
},
|
||||
"Database": {
|
||||
"Provider": "sqlite"
|
||||
},
|
||||
"Photon": {
|
||||
"AppId": "",
|
||||
"VoiceAppId": "",
|
||||
"CloudRegion": "us",
|
||||
"RequireCustomAuth": false
|
||||
},
|
||||
"Server": {
|
||||
"MOTD": "DorkNet 2018 local test server"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user