Enhance room bulk lookup: add support for ID-based queries and improve result handling; refactor GoToController for presence management and logging; update join timeout service to handle presence deferral; add Crescendo image asset.

This commit is contained in:
Alexa
2026-06-03 10:53:44 +03:00
parent ff0fb65505
commit 0011ae1039
7 changed files with 119 additions and 115 deletions
@@ -909,22 +909,30 @@ public class RoomsController(
return Ok(BuildRoomServerDetails(room, scenes, roles: roomRoles));
}
/// <summary>GET <c>/roomserver/rooms/bulk?name=X&amp;name=Y</c> — bulk
/// lookup by room name. Used by EJDCNGBEICB's room cache to resolve
/// well-known names (RecCenter, etc.) to their RoomServerDetails
/// shape in one round-trip. Returns the same per-room object that
/// <c>/roomserver/rooms/{id}</c> emits, one per requested name; names
/// we can't resolve are silently skipped (watch handles a shorter
/// <summary>GET <c>/roomserver/rooms/bulk?name=X&amp;name=Y</c> or
/// <c>?id=1&amp;id=2</c> — bulk room cache lookup. Decomp-verified
/// in EJDCNGBEICB: the Int64 overload writes query key <c>id</c>
/// and the string overload writes <c>name</c>. Returns the same
/// per-room object that <c>/roomserver/rooms/{id}</c> emits;
/// unresolved entries are silently skipped (watch handles a shorter
/// list fine, but a 404 wedges the room browser).</summary>
[HttpGet("roomserver/rooms/bulk")]
[HttpGet("rooms/bulk")]
public async Task<IActionResult> RoomServerBulkByName()
{
var names = Request.Query["name"].Where(n => !string.IsNullOrWhiteSpace(n)).ToArray();
if (names.Length == 0) return Ok(Array.Empty<object>());
var ids = Request.Query["id"]
.Where(v => !string.IsNullOrWhiteSpace(v))
.SelectMany(v => v!.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.Select(v => long.TryParse(v, out var id) ? id : 0)
.Where(id => id > 0)
.Distinct()
.ToArray();
if (names.Length == 0 && ids.Length == 0) return Ok(Array.Empty<object>());
var pid = CurrentPlayerId ?? 0;
var results = new List<object>(names.Length);
var results = new List<object>(names.Length + ids.Length);
var emitted = new HashSet<long>();
foreach (var raw in names)
{
var name = raw!.Trim();
@@ -948,6 +956,34 @@ public class RoomsController(
overrideName: DormNameOverride(name));
}
var scenes = await db.RoomScenes
.Where(s => s.RoomId == room.Id)
.OrderBy(s => s.OrderIndex)
.ToListAsync();
var roomRoles = await db.RoomRoles
.Where(r => r.RoomId == room.Id)
.ToListAsync();
results.Add(BuildRoomServerDetails(room, scenes, roles: roomRoles));
emitted.Add(room.Id);
}
foreach (var id in ids)
{
if (!emitted.Add(id)) continue;
var room = await rooms.GetByIdAsync(id);
if (room is null) continue;
if (room.IsDormRoom)
{
var dormBlobName = await db.DormStates
.Where(d => d.PlayerId == room.CreatorPlayerId)
.Select(d => d.CurrentDataBlobName)
.FirstOrDefaultAsync()
?? string.Empty;
room = CloneWithCreator(room, room.CreatorPlayerId, dormBlobName);
}
var scenes = await db.RoomScenes
.Where(s => s.RoomId == room.Id)
.OrderBy(s => s.OrderIndex)
@@ -703,29 +703,41 @@ public class GoToController(
private async Task RecordResponseAsync(MatchmakingResponseDto response)
{
if (this.CurrentPlayerId() is not long playerId) return;
var room = response.RoomInstance;
if (room is null) return;
if (response.RoomInstance is null) return;
var previousRoom = presence.GetRoom(playerId);
var additionalPlayerIds = await GetEligibleAdditionalPlayerIdsAsync(playerId, previousRoom);
await RecordPlayerMoveAsync(playerId, room);
if (additionalPlayerIds.Count == 0)
return;
logger.LogInformation(
"[goto-party] player={PlayerId} moving additional players [{AdditionalPlayerIds}] to instance={InstanceId}",
playerId, string.Join(",", additionalPlayerIds), room.RoomInstanceId);
foreach (var additionalPlayerId in additionalPlayerIds)
var targetRoom = response.RoomInstance;
var currentRoom = presence.GetRoom(playerId);
var deferPresenceCommit = ShouldDeferPresenceCommit(currentRoom, targetRoom);
joinTimeouts.MarkPending(playerId, targetRoom, deferPresenceCommit);
if (deferPresenceCommit)
{
var roomForPartyMember = CloneRoomInstance(room);
if (roomForPartyMember.IsPrivate)
await privateInstances.InviteAsync(roomForPartyMember.RoomInstanceId, additionalPlayerId);
await RecordPlayerMoveAsync(additionalPlayerId, roomForPartyMember);
logger.LogInformation(
"[goto] deferred presence commit player={PlayerId} room={RoomId} subRoom={SubRoomId} fromInstance={FromInstance} toInstance={ToInstance} fromPhoton={FromPhoton} toPhoton={ToPhoton}",
playerId,
targetRoom.RoomId,
targetRoom.SubRoomId,
currentRoom?.RoomInstanceId,
targetRoom.RoomInstanceId,
currentRoom?.PhotonRoomId,
targetRoom.PhotonRoomId);
return;
}
presence.SetRoom(playerId, targetRoom);
// Real visit tracking: upsert the per-(room, player) row so
// we get distinct VisitorCount (unique players ever) AND the
// gross VisitCount (every join, including rejoins). Skip the
// canonical dorm (RoomId=1) so per-day boots don't skew it.
if (targetRoom.RoomId != 1)
await RecordVisitAsync(playerId, targetRoom.RoomId);
// Push a presence-update notification to every accepted-friend
// so their watch's "friends" list shows the new room. Cheap
// query — Relationships is small, and we only fan out to the
// actually-connected friends.
await NotifyFriendsOfMoveAsync(playerId, targetRoom);
// NOTE: we INTENTIONALLY do NOT self-push SubscriptionUpdateRoom
// from /goto. The watch's Rooms.OnSubscriptionUpdateRoom
// (Cpp2IL_ISIL/.../RecNet/Rooms.txt:2401-2496) is gated on
@@ -744,76 +756,13 @@ public class GoToController(
// first one for any (room, subRoom).
}
private async Task RecordPlayerMoveAsync(long playerId, RoomInstanceDto room)
{
presence.SetRoom(playerId, room);
joinTimeouts.MarkPending(playerId, room);
// Real visit tracking: upsert the per-(room, player) row so
// we get distinct VisitorCount (unique players ever) AND the
// gross VisitCount (every join, including rejoins). Skip the
// canonical dorm (RoomId=1) so per-day boots don't skew it.
if (room.RoomId != 1)
await RecordVisitAsync(playerId, room.RoomId);
// Push a presence-update notification to every accepted-friend
// so their watch's "friends" list shows the new room. Cheap
// query — Relationships is small, and we only fan out to the
// actually-connected friends.
await NotifyFriendsOfMoveAsync(playerId, room);
}
private async Task<IReadOnlyList<long>> GetEligibleAdditionalPlayerIdsAsync(
long callerPlayerId,
RoomInstanceDto? callerPreviousRoom)
{
if (!Request.HasFormContentType || callerPreviousRoom is null)
return Array.Empty<long>();
var form = await Request.ReadFormAsync();
var rawValues = form["AdditionalPlayerIds"]
.Concat(form["additionalPlayerIds"])
.Concat(form["additionalPlayerIds[]"])
.ToArray();
if (rawValues.Length == 0)
return Array.Empty<long>();
var ids = rawValues
.SelectMany(v => (v ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.Select(v => long.TryParse(v, out var id) ? id : 0)
.Where(id => id > 0 && id != callerPlayerId)
.Distinct()
.ToList();
if (ids.Count == 0)
return Array.Empty<long>();
var eligible = new List<long>(ids.Count);
foreach (var id in ids)
{
var current = presence.GetRoom(id);
if (current is not null && IsSameLiveInstance(current, callerPreviousRoom))
{
eligible.Add(id);
continue;
}
logger.LogWarning(
"[goto-party] ignoring AdditionalPlayerIds entry caller={CallerId} additional={AdditionalId} callerPrevious={CallerInstance} additionalCurrent={AdditionalInstance}",
callerPlayerId, id, callerPreviousRoom.RoomInstanceId, current?.RoomInstanceId);
}
return eligible;
}
private static bool IsSameLiveInstance(RoomInstanceDto a, RoomInstanceDto b)
{
if (a.RoomInstanceId == b.RoomInstanceId)
return true;
return !string.IsNullOrWhiteSpace(a.PhotonRoomId)
&& string.Equals(a.PhotonRoomId, b.PhotonRoomId, StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldDeferPresenceCommit(RoomInstanceDto? currentRoom, RoomInstanceDto targetRoom) =>
currentRoom is not null
&& targetRoom.IsPrivate
&& currentRoom.RoomId == targetRoom.RoomId
&& currentRoom.SubRoomId == targetRoom.SubRoomId
&& (currentRoom.RoomInstanceId != targetRoom.RoomInstanceId
|| !string.Equals(currentRoom.PhotonRoomId, targetRoom.PhotonRoomId, StringComparison.OrdinalIgnoreCase));
/// <summary>Mutate the response so it represents a fresh invite-only
/// match: forks the PhotonRoomId with a per-(player, nonce) suffix
@@ -52,7 +52,18 @@ public class MatchController(
var pid = this.RequireCurrentPlayerId();
if (code == 0)
{
joinTimeouts.MarkCompleted(pid, instanceId, label);
var completed = joinTimeouts.MarkCompleted(pid, instanceId, label);
if (completed.HasValue)
{
presence.SetRoom(pid, completed.Value.TargetRoom);
logger.LogInformation(
"[joinresult] committed presence player={Player} room={RoomId}/{RoomName} instance={Instance} deferred={Deferred}",
pid,
completed.Value.TargetRoom.RoomId,
completed.Value.TargetRoom.Name,
completed.Value.TargetRoom.RoomInstanceId,
completed.Value.DeferPresenceCommit);
}
logger.LogInformation(
"[joinresult] player={Player} instance={Instance} OK ({Label})",
pid, instanceId, label);