mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: implement the client routes that had no handler
Sixteen agents, one controller each, over every route the coverage test reported as missing or wrong-verb. Route+verb coverage is now 516/522 (98.9%), up from 90.8%. Each handler was written against binary evidence rather than the audit's prose: verb from the BestHTTP.HTTPMethods ordinal at the call site, response shape from the issuing method's return type, request shape from whether the client uses form fields, a raw JsonUtility body, or query params. Agents were told to skip rather than stub, and several did. New surface worth noting: - Link host: actionlink create/get/consume and datalink get/put, previously unrouted entirely. - Club roster reads: members, banned, requests, requests/search, members/search, each with the client's real sort/paging params and the wire row shape its reader expects (the banned row has no MembershipType field, so it isn't sent). - Home clubhouse set/clear, club disband (soft, via the existing State column), and DELETE mainimage as its own action rather than a verb alias — the client parses the details envelope from it, so it is not status-only. - Club chat threads, comment CRUD, keepsake collect/delete, progression-event xp boosts, image report and cheered/bulk, iOS StoreKit product reporting. The six routes still missing all need storage that does not exist yet (per-role club permissions, club additional-image slots, MinLevel, ClubChatEnabled). Those are left for a single schema change rather than faked. Also excludes DELETE /account/me from the endpoint contract probe. That route is now a real, correctly-guarded account purge, and the probe walks every route with one test player — so calling it destroyed the probe's own session and turned one real event into 49 cascading foreign-key and 401 failures.
This commit is contained in:
@@ -33,7 +33,8 @@ namespace DorkNet.Server.Controllers.API.PlayerEvents;
|
||||
[Authorize]
|
||||
public class PlayerEventsController(
|
||||
DorkNetDbContext db,
|
||||
NotificationService notifications) : ControllerBase
|
||||
NotificationService notifications,
|
||||
PlayerPresenceService presence) : ControllerBase
|
||||
{
|
||||
private long Me => this.RequireCurrentPlayerId();
|
||||
|
||||
@@ -351,6 +352,101 @@ public class PlayerEventsController(
|
||||
return Ok(rows.Select(ResponseWire).ToList());
|
||||
}
|
||||
|
||||
// ── Event instance browser ───────────────────────────────────────────
|
||||
|
||||
/// <summary>GET <c>event/{eventId}/instances</c> — the "which instance of
|
||||
/// this event are my friends in" browser behind the event card's join
|
||||
/// button. Served from the MATCHMAKING-HOST root (no <c>api/</c> prefix),
|
||||
/// which is why it lives on an absolute route here even though the feature
|
||||
/// is a player-event one.
|
||||
///
|
||||
/// Binary evidence: <c>Matchmaking+<GetEventInstanceBrowser>b__0</c>
|
||||
/// (<c>Matchmaking_NestedType_CBMHJMNIHNN.txt:14</c>) formats the literal
|
||||
/// <c>"event/{0}/instances"</c> at <c>:296</c> with <c>[rdi+16]</c> — the
|
||||
/// PlayerEventId off the HPIOAGDJHDH the lambda closes over — then news up
|
||||
/// BNDIAONDFFF with <c>rdx = 0</c> at <c>:124</c>, i.e.
|
||||
/// <c>BestHTTP.HTTPMethods.Get</c>. The declared return type is
|
||||
/// <c>FGLDKEJLAKB<List<PNDCMIMEJLD>></c>, so the body is a BARE
|
||||
/// JSON ARRAY. PNDCMIMEJLD's getters (<c>PNDCMIMEJLD.txt:3-197</c>) are, in
|
||||
/// order, Int64/Int64/Int64/Boolean/DateTime/List<Int32>/String/Int32/
|
||||
/// Boolean/String = RoomInstanceId, RoomId, SubRoomId, IsFull, CreatedAt,
|
||||
/// PlayerIds, SubroomName, PlayerCount, HasModPresent, HashedInstanceId —
|
||||
/// byte-for-byte the SimpleRoomInstance the <c>room/{id}/instances</c>
|
||||
/// browser already returns (<c>Matchmaking.txt:14237</c> issues the room
|
||||
/// variant through the identical BNDIAONDFFF/verb-0 sequence). The last
|
||||
/// four are filled in locally on the watch and are not read back off the
|
||||
/// wire, so we emit the same six camelCase keys
|
||||
/// (<c>Controllers/Match/MatchController.cs:545-553</c>); the 2023 Utf8Json
|
||||
/// formatters carry a camelCase variant per key, so both client
|
||||
/// generations parse it.
|
||||
///
|
||||
/// The event itself only supplies the room to browse: an event is pinned to
|
||||
/// one RoomId, plus the SubRoomId the host picked in the v2 create/edit body
|
||||
/// (persisted in <see cref="EventExtras"/>). Sub-room hops allocate their
|
||||
/// own instance ids and photon rooms, so when the event names a sub-room we
|
||||
/// only surface instances sitting in it.</summary>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("/event/{eventId:long}/instances")]
|
||||
public async Task<IActionResult> EventInstances(long eventId)
|
||||
{
|
||||
var evt = await db.PlayerEvents.FirstOrDefaultAsync(e => e.Id == eventId);
|
||||
if (evt is null) return NotFound();
|
||||
|
||||
var extras = (await LoadExtrasAsync(new[] { evt }))[evt.Id];
|
||||
var subRoomId = extras.SubRoomId is long sub && sub > 0 ? sub : (long?)null;
|
||||
|
||||
// Same sourcing as the room browser: presence rows are the live truth
|
||||
// (one instance per distinct PhotonRoomId), and the caller's OWN private
|
||||
// instances are merged in so the host can still see the instance they
|
||||
// created before anyone has joined it. Another player's private
|
||||
// PhotonRoomId is never listed.
|
||||
var pid = this.CurrentPlayerId() ?? 0;
|
||||
var privates = pid == 0
|
||||
? new List<PrivateInstanceEntity>()
|
||||
: await db.PrivateInstances
|
||||
.Where(p => p.RoomId == evt.RoomId && p.OwnerPlayerId == pid)
|
||||
.ToListAsync();
|
||||
|
||||
var byKey = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||
foreach (var (room, playerIds) in presence.EnumerateActiveInstances(evt.RoomId))
|
||||
{
|
||||
if (string.IsNullOrEmpty(room.PhotonRoomId)) continue;
|
||||
if (subRoomId is long want && room.SubRoomId != want) continue;
|
||||
byKey[room.PhotonRoomId] = new
|
||||
{
|
||||
roomInstanceId = room.RoomInstanceId,
|
||||
roomId = room.RoomId,
|
||||
subRoomId = room.SubRoomId,
|
||||
// MaxCapacity is the per-instance cap the matchmaker handed
|
||||
// out, so this is a real answer rather than a constant false.
|
||||
isFull = room.MaxCapacity > 0 && playerIds.Count >= room.MaxCapacity,
|
||||
// Presence rows are a TTL cache with no creation timestamp —
|
||||
// there is no stored "instance opened at" for a live instance,
|
||||
// so report the observation time. Private-instance rows below
|
||||
// do carry a real CreatedAt and use it.
|
||||
createdAt = DateTime.UtcNow,
|
||||
playerIds = playerIds.Select(p => (int)p).ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var p in privates)
|
||||
{
|
||||
if (string.IsNullOrEmpty(p.PhotonRoomId) || byKey.ContainsKey(p.PhotonRoomId)) continue;
|
||||
if (subRoomId is long want && p.SubRoomId != want) continue;
|
||||
byKey[p.PhotonRoomId] = new
|
||||
{
|
||||
roomInstanceId = p.Id,
|
||||
roomId = p.RoomId,
|
||||
subRoomId = p.SubRoomId,
|
||||
isFull = false,
|
||||
createdAt = DateTime.SpecifyKind(p.CreatedAt, DateTimeKind.Utc),
|
||||
playerIds = Array.Empty<int>(),
|
||||
};
|
||||
}
|
||||
|
||||
return Ok(byKey.Values.ToList());
|
||||
}
|
||||
|
||||
// ── Wire shape ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>The PlayerEvent fields the 2023 client carries that
|
||||
|
||||
Reference in New Issue
Block a user