mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 23:48:45 -07:00
fix: presence, profile edits, outfit menu and RecNetResult writes for the 2023 client
Every change below was checked against the 2023-03-21 client disassembly;
file:line evidence is in the code comments and in
docs/recroom-2023-client-api-complete.md.
Presence
- matchmake/none means "online, in no room" (its only caller is
TryGoToOfflineRoom, which ignores the reply). The server used to keep the
old room or invent a dorm, so the next heartbeat looked out of sync and
the client cancelled its load to chase that room.
- The join watchdog no longer clears presence 30 s into a long room load;
it is a 3 minute backstop that drops the room only.
- One fan-out (PresenceBroadcastService) for every transition: deferred
joins, matchmake/none, logout, failed joins, visibility changes, SignalR
connect/disconnect. isOnline is sent on its own; pushes never go to the
subject, because a self push moves the player.
- Online-status visibility (Public / Friends / Favorites / Appear offline)
is stored and honoured by GET /player and by the live pushes.
Profile
- PUT account/me/{emoji,personalpronouns,identityflags,bannerimage} were
404; added with the client's real form keys and persisted (new Players
columns plus migration). Every account read now returns them, and edits
push AccountUpdate to everyone.
Outfit menu
- Saved outfits kept OutfitSelectionsV2 and CustomAvatarItems, so loading a
slot restores what was saved instead of a stale outfit.
- isCreationAllowedForAccount answers the {Success, Error} envelope the
client parses; minPriceForPublicItem is a bare int; gifts/generate accepts
form bodies; lockeditems returns the full owned-item shape; colourways no
longer share their base item's AvatarItemId.
RecNetResult writes
- About 40 writes look status-only but parse the body as {Success, Error}.
An empty 200 showed "Failed to ..." after the server applied the change,
and a body without Success threw "Error message cannot be null". Clubs,
announcements, keepsakes, comments/read, subscriptions, purchase offers
and privacy settings now answer RecNetResult.
- Login token and device-code grants are real; remoteauth approves the
device code; createlogintoken returns a bare string.
- New: incentivizedreferrals/referrals, account/bulk/{email,phone},
GET strings, account/me/confirmphone, PlayMenuCarouselOrdering_1.
Route inventory and coverage test extended to match.
README now describes the 2023 branch.
This commit is contained in:
@@ -48,6 +48,7 @@ public class PlayerPresenceService
|
||||
private readonly ConcurrentDictionary<long, DateTimeOffset> _localRoomExpires = new();
|
||||
private readonly ConcurrentDictionary<long, DateTimeOffset> _localActiveExpires = new();
|
||||
private readonly ConcurrentDictionary<long, DateTimeOffset> _localBootDormGraceExpires = new();
|
||||
private readonly ConcurrentDictionary<long, DateTimeOffset> _localNoRoomExpires = new();
|
||||
private readonly IConnectionMultiplexer? _redis;
|
||||
private readonly ILogger<PlayerPresenceService>? _log;
|
||||
|
||||
@@ -101,7 +102,9 @@ public class PlayerPresenceService
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(room, JsonOpts);
|
||||
mux.GetDatabase().StringSet(Key(playerId), json, PresenceTtl);
|
||||
var db = mux.GetDatabase();
|
||||
db.StringSet(Key(playerId), json, PresenceTtl);
|
||||
db.KeyDelete(NoRoomKey(playerId));
|
||||
_log?.LogDebug("[presence] set redis player={PlayerId} room={Room} bytes={Bytes}",
|
||||
playerId, room?.Name ?? "<null>", json.Length);
|
||||
}
|
||||
@@ -117,6 +120,7 @@ public class PlayerPresenceService
|
||||
{
|
||||
_local[playerId] = room;
|
||||
_localRoomExpires[playerId] = DateTimeOffset.UtcNow.Add(PresenceTtl);
|
||||
_localNoRoomExpires.TryRemove(playerId, out _);
|
||||
_log?.LogDebug("[presence] set local player={PlayerId} room={Room}",
|
||||
playerId, room?.Name ?? "<null>");
|
||||
}
|
||||
@@ -333,6 +337,7 @@ public class PlayerPresenceService
|
||||
db.KeyDelete(Key(playerId));
|
||||
db.KeyDelete(ActiveKey(playerId));
|
||||
db.KeyDelete(BootDormGraceKey(playerId));
|
||||
db.KeyDelete(NoRoomKey(playerId));
|
||||
db.SetRemove(ActiveSetKey, playerId);
|
||||
}
|
||||
else
|
||||
@@ -341,9 +346,119 @@ public class PlayerPresenceService
|
||||
_localRoomExpires.TryRemove(playerId, out _);
|
||||
_localActiveExpires.TryRemove(playerId, out _);
|
||||
_localBootDormGraceExpires.TryRemove(playerId, out _);
|
||||
_localNoRoomExpires.TryRemove(playerId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Forget the player's room but keep everything that says they
|
||||
/// are still here (activity marker, login lock, boot grace). Used when a
|
||||
/// join fails or times out: the player is still online and about to run
|
||||
/// the client's fallback chain, so friends must not see them go offline,
|
||||
/// they just must not see a room the player never reached.</summary>
|
||||
public void ClearRoom(long playerId)
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
try
|
||||
{
|
||||
mux.GetDatabase().KeyDelete(Key(playerId));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.LogWarning(ex, "[presence] redis room DEL failed for player={PlayerId}", playerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_local.TryRemove(playerId, out _);
|
||||
_localRoomExpires.TryRemove(playerId, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record that the player deliberately left online matchmaking: they are
|
||||
/// online, but in no room instance.
|
||||
///
|
||||
/// The 2023 client does this in TryGoToOfflineRoom
|
||||
/// (RecNet.Runtime/RecNet/Matchmaking.txt:12677, the only caller of
|
||||
/// <c>matchmake/none</c> at :13279): it swaps its cached presence to the
|
||||
/// local offline instance (RoomInstanceId -1) and posts matchmake/none,
|
||||
/// ignoring the reply (<c><TryGoToOfflineRoom>b__119_0</c> returns a
|
||||
/// constant). From then on the heartbeat handler
|
||||
/// (Matchmaking.txt:18103, compare at :18415-18467) treats a negative RoomInstanceId like no
|
||||
/// room (HCHDEHIGEBE.GIICPDHJOOH = <c>RoomInstanceId < 0</c>) and is
|
||||
/// only in sync while the server answers <c>roomInstance: null</c>. Any
|
||||
/// room here — the old one, or a synthesised dorm — reads as out-of-sync
|
||||
/// and the client cancels its offline load to chase the server's room.
|
||||
/// The marker keeps the heartbeat's dorm fallback from firing for this
|
||||
/// player until they matchmake somewhere again.
|
||||
/// </summary>
|
||||
public void SetNoRoom(long playerId)
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
try
|
||||
{
|
||||
var db = mux.GetDatabase();
|
||||
db.KeyDelete(Key(playerId));
|
||||
db.StringSet(NoRoomKey(playerId), "1", PresenceTtl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.LogWarning(ex, "[presence] redis no-room SET failed for player={PlayerId}", playerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_local.TryRemove(playerId, out _);
|
||||
_localRoomExpires.TryRemove(playerId, out _);
|
||||
_localNoRoomExpires[playerId] = DateTimeOffset.UtcNow.Add(PresenceTtl);
|
||||
}
|
||||
|
||||
/// <summary>True while the player is online with no room after
|
||||
/// <see cref="SetNoRoom"/>. Cleared by the next <see cref="SetRoom"/> or
|
||||
/// <see cref="Clear"/>.</summary>
|
||||
public bool IsInNoRoom(long playerId)
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
try
|
||||
{
|
||||
return mux.GetDatabase().KeyExists(NoRoomKey(playerId));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.LogWarning(ex, "[presence] redis no-room GET failed for player={PlayerId}", playerId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_localNoRoomExpires.TryGetValue(playerId, out var expires)) return false;
|
||||
if (expires > DateTimeOffset.UtcNow) return true;
|
||||
_localNoRoomExpires.TryRemove(playerId, out _);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Keep the no-room marker alive while the player keeps
|
||||
/// heartbeating from the offline room.</summary>
|
||||
public void TouchNoRoom(long playerId)
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
try
|
||||
{
|
||||
mux.GetDatabase().KeyExpire(NoRoomKey(playerId), PresenceTtl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log?.LogWarning(ex, "[presence] redis no-room EXPIRE failed for player={PlayerId}", playerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_localNoRoomExpires.ContainsKey(playerId))
|
||||
_localNoRoomExpires[playerId] = DateTimeOffset.UtcNow.Add(PresenceTtl);
|
||||
}
|
||||
|
||||
/// <summary>List the currently-active instances of a given room,
|
||||
/// grouped by PhotonRoomId, with the player ids currently presenced
|
||||
/// in each. Drives the watch's RoomInstanceBrowser
|
||||
@@ -386,6 +501,7 @@ public class PlayerPresenceService
|
||||
private static string Key(long playerId) => $"presence:{playerId}";
|
||||
private static string ActiveKey(long playerId) => $"presence-active:{playerId}";
|
||||
private static string BootDormGraceKey(long playerId) => $"presence-boot-dorm-grace:{playerId}";
|
||||
private static string NoRoomKey(long playerId) => $"presence-noroom:{playerId}";
|
||||
private const string ActiveSetKey = "presence-active";
|
||||
|
||||
// ── LoginLock single-session enforcement ─────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user