From ff0fb655055e4e9ba4c4b842e2a0ad27f97ab209 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Wed, 3 Jun 2026 10:33:59 +0300 Subject: [PATCH] Enhance GoToRoom method: add JoinMode handling for instance creation and improve player movement tracking --- .../Controllers/Match/GoToController.cs | 121 +++++++++++++++--- 1 file changed, 106 insertions(+), 15 deletions(-) diff --git a/DorkNet.Server/Controllers/Match/GoToController.cs b/DorkNet.Server/Controllers/Match/GoToController.cs index 69b1dbb..f5327d8 100644 --- a/DorkNet.Server/Controllers/Match/GoToController.cs +++ b/DorkNet.Server/Controllers/Match/GoToController.cs @@ -42,7 +42,22 @@ public class GoToController( [HttpPost("/goto/room/{roomName}")] public async Task> GoToRoom( string roomName, - [FromForm(Name = "CreatePrivateInstance")] bool createPrivateInstance = false) + [FromForm(Name = "CreatePrivateInstance")] bool createPrivateInstance = false, + // The December watch signals "make a new instance" via the + // JoinMode form field, NOT CreatePrivateInstance (which it never + // sends — verified on the wire: a "New Private Instance" request + // carries `JoinMode=2` and no CreatePrivateInstance key). Maps to + // RecNet.Matchmaking's GIJOGKJCEKG enum: + // 0 = PublicMatchmaking (join the existing shared instance) + // 1 = PublicNewInstance (fork a fresh PUBLIC instance) + // 2 = PrivateNewInstance (fork a fresh, invite-gated instance) + // Without honouring this, JoinMode 1/2 fell through to the + // default branch and returned the SAME roomInstanceId+photonRoomId + // the player was already standing in. Per the sub-room note below, + // identical instance id + photon room => the watch's transition + // state machine never fires and "Going to " hangs forever + // (the player softlocks; the join-watchdog then times out at 30s). + [FromForm(Name = "JoinMode")] int joinMode = 0) { // Unknown room name shouldn't silently drop the player into a // synth'd dorm scene any more — that masked typo bug reports @@ -688,23 +703,28 @@ public class GoToController( private async Task RecordResponseAsync(MatchmakingResponseDto response) { if (this.CurrentPlayerId() is not long playerId) return; - if (response.RoomInstance is null) return; + var room = response.RoomInstance; + if (room is null) return; - presence.SetRoom(playerId, response.RoomInstance); - joinTimeouts.MarkPending(playerId, response.RoomInstance); + var previousRoom = presence.GetRoom(playerId); + var additionalPlayerIds = await GetEligibleAdditionalPlayerIdsAsync(playerId, previousRoom); + await RecordPlayerMoveAsync(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 (response.RoomInstance.RoomId != 1) - await RecordVisitAsync(playerId, response.RoomInstance.RoomId); + if (additionalPlayerIds.Count == 0) + return; - // 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, response.RoomInstance); + 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 roomForPartyMember = CloneRoomInstance(room); + if (roomForPartyMember.IsPrivate) + await privateInstances.InviteAsync(roomForPartyMember.RoomInstanceId, additionalPlayerId); + + await RecordPlayerMoveAsync(additionalPlayerId, roomForPartyMember); + } // NOTE: we INTENTIONALLY do NOT self-push SubscriptionUpdateRoom // from /goto. The watch's Rooms.OnSubscriptionUpdateRoom @@ -724,6 +744,77 @@ 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> GetEligibleAdditionalPlayerIdsAsync( + long callerPlayerId, + RoomInstanceDto? callerPreviousRoom) + { + if (!Request.HasFormContentType || callerPreviousRoom is null) + return Array.Empty(); + + var form = await Request.ReadFormAsync(); + var rawValues = form["AdditionalPlayerIds"] + .Concat(form["additionalPlayerIds"]) + .Concat(form["additionalPlayerIds[]"]) + .ToArray(); + if (rawValues.Length == 0) + return Array.Empty(); + + 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(); + + var eligible = new List(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); + } + /// Mutate the response so it represents a fresh invite-only /// match: forks the PhotonRoomId with a per-(player, nonce) suffix /// so randos can't matchmake into it, sets isPrivate=true, and