From 28c1755d999834236acaba2cb390fcd3e53a7828 Mon Sep 17 00:00:00 2001
From: Alexa <53471801+Alexa-RR@users.noreply.github.com>
Date: Sun, 28 Jun 2026 02:15:58 +0300
Subject: [PATCH] feat: Update README with branch version and single-server
defaults; enhance inventory and room handling for equipped items
---
.../API/Inventory/InventoryController.cs | 33 ++++++++++++-
.../API/Rooms/V2/RoomsController.cs | 8 ++++
.../Match/MatchPlayerController.cs | 6 +--
.../Match/PlayerPresenceController.cs | 14 +++---
.../Services/NotificationService.cs | 3 +-
README.md | 47 ++++++++++++++++++-
6 files changed, 99 insertions(+), 12 deletions(-)
diff --git a/DorkNet.Server/Controllers/API/Inventory/InventoryController.cs b/DorkNet.Server/Controllers/API/Inventory/InventoryController.cs
index 496ddd4..5f3fb68 100644
--- a/DorkNet.Server/Controllers/API/Inventory/InventoryController.cs
+++ b/DorkNet.Server/Controllers/API/Inventory/InventoryController.cs
@@ -66,6 +66,13 @@ public class InventoryController(
public string PrefabName { get; set; } = string.Empty;
public string ModificationGuid { get; set; } = string.Empty;
public bool Favorited { get; set; }
+ // The 2023 client sends Equipped/IsEquipped when a weapon skin is
+ // equipped (distinct from Favorited). We only have one IsActive flag,
+ // and for skins "active" == "equipped", so honor whichever the client
+ // sent. Without this the equip was dropped (only Favorited was read)
+ // and the skin never stuck.
+ public bool? Equipped { get; set; }
+ public bool? IsEquipped { get; set; }
}
/// POST api/equipment/v1/update — toggle the
@@ -103,7 +110,27 @@ public class InventoryController(
// to themselves).
return Forbid();
}
- row.IsActive = req.Favorited;
+ // "equipped" is the meaningful state for weapon skins; prefer it,
+ // fall back to Favorited for plain tool-pin updates.
+ var active = req.Equipped ?? req.IsEquipped ?? req.Favorited;
+
+ // One skin equipped per weapon: when equipping a catalog-backed skin,
+ // clear the equipped flag on the player's OTHER skins for the SAME
+ // prefab (slugs share the "{prefix}{prefab}:" head) so the weapon
+ // doesn't end up with two "equipped" skins.
+ if (active && !string.IsNullOrWhiteSpace(req.ModificationGuid)
+ && !string.IsNullOrWhiteSpace(req.PrefabName))
+ {
+ var prefabPrefix = StoreService.EquipmentSkinSlug(req.PrefabName, string.Empty);
+ var siblings = await db.PlayerInventory
+ .Where(inv => inv.PlayerId == pid && inv.IsActive
+ && inv.ItemSlug != slug
+ && inv.ItemSlug.StartsWith(prefabPrefix))
+ .ToListAsync();
+ foreach (var sib in siblings) sib.IsActive = false;
+ }
+
+ row.IsActive = active;
await db.SaveChangesAsync();
return Ok(new { Updated = true });
}
@@ -261,7 +288,11 @@ public class InventoryController(
FriendlyName = item.DisplayName,
Tooltip = item.Description,
Rarity = rarity,
+ // IsActive is the equipped/active flag; surface it under every key
+ // the 2023 client reads so the equipped skin shows on reload.
Favorited = inv.IsActive,
+ Equipped = inv.IsActive,
+ IsEquipped = inv.IsActive,
};
}
diff --git a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsController.cs b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsController.cs
index 2c7b005..0580cbc 100644
--- a/DorkNet.Server/Controllers/API/Rooms/V2/RoomsController.cs
+++ b/DorkNet.Server/Controllers/API/Rooms/V2/RoomsController.cs
@@ -2534,6 +2534,8 @@ public class RoomsController(
/// verifying ownership and intra-room uniqueness.
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
[HttpPut("rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
+ [HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
+ [HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
[Authorize]
public async Task RenameSubroom(
long roomId,
@@ -3353,8 +3355,13 @@ public class RoomsController(
return Ok(SceneWire(clone));
}
+ // Subroom settings save POSTs here on the roomserver host ("failed to
+ // modify room" = the bare-only routes 404'd). Same roomserver/ prefix as
+ // the sibling subroom mutations.
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
[HttpPut("rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
+ [HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
+ [HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
[Authorize]
public Task ModifySubRoom(long roomId, long subRoomId, [FromBody] SubRoomModifyRequest req) =>
MutateScene(roomId, subRoomId, s =>
@@ -3370,6 +3377,7 @@ public class RoomsController(
/// scene. Body NewIndex is the target OrderIndex; the in-between
/// rows shift to fill the gap.
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
+ [HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
[Authorize]
public async Task MoveSubRoom(long roomId, long subRoomId, [FromBody] SubRoomMoveRequest req)
{
diff --git a/DorkNet.Server/Controllers/Match/MatchPlayerController.cs b/DorkNet.Server/Controllers/Match/MatchPlayerController.cs
index acfb36c..fd96f96 100644
--- a/DorkNet.Server/Controllers/Match/MatchPlayerController.cs
+++ b/DorkNet.Server/Controllers/Match/MatchPlayerController.cs
@@ -158,7 +158,7 @@ public class MatchPlayerController(
return Ok(new PresenceDto
{
PlayerId = playerId,
- StatusVisibility = 1,
+ StatusVisibility = 0, // Everyone (was 1 = FriendsOnly)
DeviceClass = 0,
VrMovementMode = 0,
RoomInstance = room,
@@ -174,7 +174,7 @@ public class MatchPlayerController(
return Ok(new PresenceDto
{
PlayerId = playerId,
- StatusVisibility = value ?? 1,
+ StatusVisibility = value ?? 0, // default Everyone; honor explicit client choice
DeviceClass = 0,
VrMovementMode = 0,
RoomInstance = room,
@@ -190,7 +190,7 @@ public class MatchPlayerController(
return Ok(new PresenceDto
{
PlayerId = playerId,
- StatusVisibility = 1,
+ StatusVisibility = 0, // Everyone (was 1 = FriendsOnly)
DeviceClass = 0,
VrMovementMode = value ?? 0,
RoomInstance = room,
diff --git a/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs b/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs
index c832966..1942b42 100644
--- a/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs
+++ b/DorkNet.Server/Controllers/Match/PlayerPresenceController.cs
@@ -98,12 +98,14 @@ public class PlayerPresenceController(
return new PlayerPresenceDto
{
PlayerId = (int)id,
- // StatusVisibility is the player's *preference* (would
- // they like to appear online if they were online?). For
- // offline players we still report Visible so when they
- // come back the friend's cached preference is correct,
- // and IsOnline=false is what gates the "Online" badge.
- StatusVisibility = 1, // Visible
+ // StatusVisibility is the player's *preference* for who may
+ // see them online. The enum is Everyone=0 / FriendsOnly=1 /
+ // Nobody=2 — so the previous value 1 (mislabeled "Visible")
+ // actually meant FriendsOnly, and anyone who wasn't your
+ // friend saw you as OFFLINE. On a private server everyone
+ // should see everyone, so report Everyone (0). IsOnline=false
+ // still gates the "Online" badge for offline players.
+ StatusVisibility = 0, // Everyone
DeviceClass = 0, // Desktop / unknown — not VR
VrMovementMode = 0,
// Full instance details are safe, and necessary, when the
diff --git a/DorkNet.Server/Services/NotificationService.cs b/DorkNet.Server/Services/NotificationService.cs
index 0fdfe73..842c2c3 100644
--- a/DorkNet.Server/Services/NotificationService.cs
+++ b/DorkNet.Server/Services/NotificationService.cs
@@ -270,7 +270,8 @@ public class NotificationService(
var msg = new System.Text.Json.Nodes.JsonObject
{
["playerId"] = (int)playerId,
- ["statusVisibility"] = 1,
+ // Everyone=0 (was 1=FriendsOnly, which hid you from non-friends).
+ ["statusVisibility"] = 0,
["deviceClass"] = 0,
["vrMovementMode"] = 0,
["roomInstance"] = redacted is null
diff --git a/README.md b/README.md
index 9bfd528..439e2f0 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# DorkNet — `december-2020-12-18`
+# DorkNet — `march-2023-03-21`
This is the **December 2020.12.18** branch of DorkNet — a self-hostable
reimplementation of the Rec Room backend tuned to the wire protocol of
@@ -298,6 +298,51 @@ and room-move presence fan-out.
---
+## Single-server defaults (review before going public)
+
+Several values are hardcoded or defaulted for the convenience of running a
+small, trusted, single-operator server. They work out of the box for a group
+of friends but should be reviewed / adapted before exposing the server to the
+public internet:
+
+- **Client build version (`appVersion`).** Hardcoded to this branch's client
+ build (`20230317`) in the presence/match/notification responses
+ (`MatchPlayerController`, `PlayerPresenceController`, `NotificationService`,
+ `RoomsController`). It must match the build everyone runs, or players get a
+ "version mismatch" and can't join each other. If you serve a different
+ client build, change it (ideally hoist it to one shared constant).
+- **No real authentication.** Accounts are keyed by a per-install `deviceId`
+ the client generates (`PlayerService.GetOrCreateByDeviceAsync`) — **not** a
+ verified identity (Steam ID is deliberately ignored so emulators work).
+ Anyone who can reach the server with a patched client gets/creates an
+ account. Use **Settings → Server → disable signups** + the
+ [signup-code](#signup-codes) flow to gate who can join; there is no password
+ wall by default.
+- **First account becomes admin.** The first account created on a fresh DB is
+ granted `IsAdmin` so there's always a root admin. On a public server, create
+ your admin account first and verify no one else beat you to it.
+- **Online status = Everyone.** Presence reports `StatusVisibility = 0`
+ (Everyone) so every player appears online to every other player, matching
+ the [everyone-is-friends](#everyone-is-friends-toggle) convenience. A public
+ server may instead want to persist and honor each player's own visibility
+ preference.
+- **Room / matchmaking capacity.** New matchmade sessions and subrooms default
+ to **8** players and are capped at **40** (`GameSessionService`,
+ `RoomSceneEntity.MaxPlayers`, admin `MaxCapacity` clamp). The in-game "Max
+ Player Count in This Subroom" slider raises it per room (slider ceiling 40).
+- **Avatar unlock clamp.** The wardrobe clamp defaults high
+ (`Avatar:MaxUnlockedItems` 4000 / per-slot 1000) for this desktop client —
+ tune in config if you change the ownership model.
+- **Hosts / domain.** The apex and per-service subdomains come from
+ `DomainConfig` (`Domain:Apex`) / `appsettings`; set these to your own
+ domain. The `Everyone-is-friends`, signup-disable, and similar switches live
+ in **admin Settings**, not source.
+
+None of these are security boundaries as shipped — treat the server as trusted
+LAN/friends infrastructure unless you harden the items above.
+
+---
+
## Contributing
See [main's CONTRIBUTING guide](../../blob/main/CONTRIBUTING.md) for