- Room roles: list the creator as Creator (255), not CoOwner (30). The
client takes the local role from the Roles list and only lets a player
edit roles below their own (Creator excepted), so the creator could not
edit Co-Owner and Creator permissions. Enforce the role ladder on the
server (RoomRoleRules), let invitees accept (PUT roles/{me} role=30) and
reject (PUT roles/{me}/invite role=0), send message Types 62/60/61, and
push RoomUpdate after every role change.
- Permissions: persist PUT subrooms/{sub}/permissions cells in a new
RoomRolePermissions table, serve them from photon_access_token (plus a
roomserver/ twin) and push PhotonAccessToken to the sub-room.
- Publish: player-owned rooms are never IsRRO on the wire; the client
hides Publish and every room setting for an RRO.
- Sub-rooms: wire SubRoomId = OrderIndex + 1 (the client will not load a
sub-room with id 0), new rooms always get a named Home sub-room, and a
startup pass repairs rooms without one.
- Instance browser: per-room Redis presence index, live players only,
private instances for the people allowed in, real IsFull.
- Room/sub-room settings: creator or accepted co-owner everywhere.
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.
Every HTTP request the 2023-03-21 client can issue now reaches a handler under
the verb it actually uses: 522/522, with an empty KnownGaps allowlist. The
coverage test asserts it, so a regression fails the build rather than becoming
a 404 in the wild.
The six routes left after the previous pass all needed storage that did not
exist, so they are done here as one schema change rather than stubbed:
- ClubRolePermissionEntity (ClubId, MembershipType, six bools) backs
PUT club/{id}/permissions/{role}. A club with no stored row falls back to the
derived default policy, so every club that predates the table behaves exactly
as before. Partial writes keep the values they currently resolve to, so a
client that omits a field cannot silently revoke a permission.
- ClubAdditionalImageEntity (ClubId, Slot, ImageName) backs the gallery. The
details envelope now emits real {ImageName, Slot} rows instead of an empty
array, and DELETE clears a slot.
- ClubEntity.MinLevel and ClubEntity.ClubChatEnabled back their setters and are
read back by hasDisabledClubChat and the Club wire shape, which previously
reported hardcoded defaults.
- ChatMessageEntity.ModerationState backs PUT thread/message/{id}/moderate,
gated so an author can retract their own message but only club moderators and
admins can action anyone else's.
- TestCaseEntity.CommentsJson backs the QA comment endpoint, appending rather
than overwriting so a second tester's note cannot erase the first.
Also corrects the Joinability comment on ClubEntity, which documented
1=RequestToJoin / 2=InviteOnly. The real enum is Open=0, InviteOnly=1,
AskToJoin=2 — the inverted reading the requesttojoin handler was already fixed
away from, left behind where it could mislead the next change.
Audits every route the March-2023 client can call against what this server
actually serves, and fixes the defects that were breaking gameplay. Adds
docs/recroom-2023-client-api-complete.md as the reference: ~500 route
literals across 13 subsystems, with verb, request/response shape, and a
per-subsystem diff. 315 defects catalogued; the ones fixed here are listed
in its "Already fixed" table.
Path coverage was never the problem — 465 of 506 routes already had
handlers. The failures were in shape, and they cluster into four causes:
- Wrong verb. The verb is a BestHTTP.HTTPMethods ordinal (GET=0 .. PATCH=5),
confirmed from that enum's static constructor. Routes registered POST-only
while the client PUTs returned 405, which made room consumables neither
buyable nor consumable and blocked every room-key edit.
- Bare scalar wrapped in an object. The client reads `1000`, not
`{"Limit":1000}`; the issuing method signature says which. Wrapping makes
the deserializer throw and the feature dies silently.
- [FromBody] vs form-urlencoded. The client sends nearly every write as form
data, which an [ApiController] action bound [FromBody] rejects with 415
before the handler runs. This alone broke all 13 club member-management
endpoints. Adds Binding/FormOrJsonModelBinder so this is fixed once.
- Notify dispatch keys. Both enum overloads of the client's RegisterHandler
lower the id via Int32.ToString() and register only that numeric key, and
lookup is a case-sensitive TryGetValue that returns silently on a miss. We
were sending the enum NAME for most ids, so friend-request updates, forced
logout, maintenance broadcasts, live balance and consumable updates, and
invention moderation pushes all evaporated with no error anywhere.
Notable behavioural bugs fixed:
- Editing a club announcement deleted it: PUT was a third binding on the
delete handler. Split apart, and the missing create route added.
- leaderboard/CheckAndSetStat did not exist. The 2023 client has no SetStat
literal at all, so every leaderboard score it wrote was discarded.
- Setting a club clubhouse cleared it: the handler read roomId from the query
while the client sends it in the body, then wrote the resulting null.
- Club join requests were inverted (the enum is Open=0, InviteOnly=1,
AskToJoin=2), so ask-to-join clubs rejected everyone and invite-only clubs
quietly admitted them.
- Room bans were pushed as id 24, for which the client registers no handler;
they now go out as ModerationKick with IsBan, so the ban takes effect
immediately instead of at next login.
- Room-key ownership queries answered for the wrong player, and "has anyone
bought this key" asked "did I buy it" — always false for the creator.
Also persists settings that were previously acknowledged and dropped
(playlist accessibility/restrictions/level-voting/warning, per-player status
visibility, VR movement mode and avoid-juniors, custom room codes), which is
what the new columns in the migration are for.
One test asserted the old nested PriceAndCurrency response shape; the
client's consumable formatter has no such key and reads Price and
PurchaseCurrencyId flat, so the test was corrected alongside the handler.
Targets Rec Room 2020.12.18 client.
Re-import after adding:
- DorkNet.ClientMod/dorknet-clientmod.json.template — config schema
the launcher's manifest-driven patcher renders into UserData/.
- tools/build-patcher-zip.ps1 — produces the dorknet-clientpatch-{key}.zip
Release asset the launcher fetches.
- .github/workflows/release.yml — auto-publishes both Release assets
on v1-december-* tag push.
Scrubbed via maintainer-side scrub-for-public.ps1: same drop rules as
the prior import, plus build-patcher-zip.ps1 added to the tools/
allow-list so it survives the scrub.