mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Enhance UgcPurchasablesController to handle bulk requests with object arrays
- Updated Bulk method to parse incoming JSON object arrays for UGC item IDs. - Introduced ReadBulkIdsAsync to handle both object arrays and flat GUID lists. - Added synthetic placeholder responses for missing UGC items to prevent client errors. feat: Implement MatchmakeRoom and MatchmakeSubRoom endpoints in GoToController - Added MatchmakeRoom and MatchmakeSubRoom methods to facilitate joining public and sub-rooms by numeric ID. - Enhanced ApplySubRoomMutation to ensure proper state transitions for sub-rooms. - Improved logging for matchmaking actions. fix: Refactor PlayerSettingsController to handle database save exceptions - Introduced SaveSettingsChangesAsync to manage DbUpdateException for busy SQLite scenarios. - Updated methods to call SaveSettingsChangesAsync instead of direct SaveChangesAsync. chore: Update URL coverage documentation for new matchmaking endpoints - Documented new MatchmakeRoom and MatchmakeSubRoom endpoints in url-coverage.md. chore: Add default room data file for room loading - Included default_room.room to serve as a fallback for missing room data. fix: Improve SQLite concurrency handling in DatabaseBootstrap - Configured SQLite to use WAL mode and set a busy timeout for better concurrency. chore: Create SqliteErrors utility for handling SQLite exceptions - Added SqliteErrors class to encapsulate logic for detecting busy SQLite exceptions. refactor: Update RoomDataBlobService to serve default room data - Modified GetDefaultBlob to load and serve a default room blob with proper role data.
This commit is contained in:
@@ -17,14 +17,10 @@ public sealed class ImageSignatureService
|
||||
?? Environment.GetEnvironmentVariable("DORKNET_IMAGE_SIGNING_KEY_ID")
|
||||
?? "p1");
|
||||
|
||||
// `.rec.net` in the client's `KEY:RSA:p1.rec.net` literal gets byte-rewritten
|
||||
// by patch-client.ps1 to match the server domain (e.g. `.localhost`). The
|
||||
// header suffix we emit has to match that, or the client errors as
|
||||
// "Signature malformed". By default we derive the suffix from the request
|
||||
// Host, which works for the standard same-domain patcher setup. Override
|
||||
// with ImageSigning:KeyIdHostSuffix if your client metadata was patched to
|
||||
// a different value than the server's host (e.g. running the server at
|
||||
// `dorknet.example` while the client metadata still says `localhost`).
|
||||
// 2023 compares Content-Signature's key-id against the embedded
|
||||
// KEY:RSA:p1.rec.net / d1.rec.net literals. If a specific client build
|
||||
// is bytepatched to another suffix, ImageSigning:KeyIdHostSuffix can
|
||||
// override this default.
|
||||
configuredKeyIdHostSuffix =
|
||||
config["ImageSigning:KeyIdHostSuffix"]
|
||||
?? Environment.GetEnvironmentVariable("DORKNET_IMAGE_SIGNING_KEY_ID_SUFFIX");
|
||||
@@ -272,19 +268,12 @@ public sealed class ImageSignatureService
|
||||
|
||||
var suffix = configuredKeyIdHostSuffix is { Length: > 0 }
|
||||
? configuredKeyIdHostSuffix
|
||||
: ExtractParentDomain(response.HttpContext.Request.Host.Host);
|
||||
: "rec.net";
|
||||
|
||||
response.Headers["Content-Signature"] =
|
||||
$"key-id=KEY:RSA:{headerKeyId}.{suffix}; data={Convert.ToBase64String(signature)}";
|
||||
}
|
||||
|
||||
private static string ExtractParentDomain(string host)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host)) return "rec.net";
|
||||
var dot = host.IndexOf('.');
|
||||
return dot > 0 && dot < host.Length - 1 ? host[(dot + 1)..] : host;
|
||||
}
|
||||
|
||||
private static byte[] PlaceholderSignature(byte[] bytes)
|
||||
{
|
||||
var signature = new byte[128];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using Google.Protobuf;
|
||||
using RecRoom.Protobuf;
|
||||
|
||||
@@ -5,40 +6,115 @@ namespace DorkNet.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Builds the binary `PersistedRoomData` protobuf the client downloads when
|
||||
/// it loads a room scene. The 2020 RecRoom client gates the Maker Pen,
|
||||
/// invention spawning, room-role editing and a dozen other tools behind
|
||||
/// `RoomRoleCollectionData.RoomRoles[i].CanUseMakerPen.InnerValue` etc.,
|
||||
/// all of which default to `false` if the bytes we serve are empty.
|
||||
///
|
||||
/// We emit a minimal blob with one PlayerRoomRoleData entry that flips
|
||||
/// every relevant `OverridableBoolData` to `(overrides=true, inner_value=true)`,
|
||||
/// which the client picks up regardless of which RoomRoleId the local
|
||||
/// player ends up assigned to. Net result: the room's creator (and
|
||||
/// everyone else, since this is a private server) gets all build/admin
|
||||
/// tools the moment they spawn into the scene.
|
||||
///
|
||||
/// Wire compatibility: our trimmed .proto only defines the messages and
|
||||
/// field numbers we set — the 2020 client's full PersistedRoomData has
|
||||
/// many more fields, but proto3 silently ignores unknown wire entries and
|
||||
/// defaults missing ones, so the bytes we generate deserialize cleanly.
|
||||
/// it loads a room scene. When a real room blob is missing, serve captured
|
||||
/// root room data with saved objects stripped and a 2023-compatible role
|
||||
/// collection instead of fabricating saved objects into the scene.
|
||||
/// </summary>
|
||||
public class RoomDataBlobService
|
||||
{
|
||||
private const int December2020PersistenceVersion = 26;
|
||||
|
||||
/// <summary>
|
||||
/// Cached bytes of the "all-permissions-on" blob. Identical for every
|
||||
/// room — there's no per-room state we vary yet, so building once at
|
||||
/// startup beats rebuilding on every download request.
|
||||
/// </summary>
|
||||
private readonly byte[] _allPermsBlob = BuildAllPermsBlob();
|
||||
private readonly RoomRoleCollectionData _allPermsRoleData = BuildAllPermsRoleData();
|
||||
private readonly byte[] _rroEditableBlob = BuildRroEditableBlob();
|
||||
private readonly RoomRoleCollectionData _rroEditableRoleData = BuildRroEditableRoleData();
|
||||
|
||||
public byte[] GetDefaultBlob() => _allPermsBlob;
|
||||
/// <summary>The blob served when a room_<id>_v1.dat misses S3
|
||||
/// (unsaved dorms / fresh customisable rooms). Uses the captured
|
||||
/// <c>data/default_room.room</c> as an input, but strips top-level
|
||||
/// persistence_views before serving it. Those views can reference
|
||||
/// prefabs absent from older clients and crash spawn with
|
||||
/// "Invalid Prefab Name: \"\""; room-role data uses the 2023 migration
|
||||
/// marker so the client does not re-run the legacy role importer.</summary>
|
||||
private readonly byte[] _defaultBlob;
|
||||
|
||||
public RoomDataBlobService()
|
||||
{
|
||||
_defaultBlob = LoadDefaultBlob();
|
||||
}
|
||||
|
||||
public byte[] GetDefaultBlob() => _defaultBlob;
|
||||
public byte[] GetRroEditableBlob() => _rroEditableBlob;
|
||||
|
||||
private static byte[] LoadDefaultBlob()
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "data", "default_room.room");
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var bytes = File.ReadAllBytes(path);
|
||||
if (bytes.Length > 0)
|
||||
{
|
||||
var msg = PersistedRoomData.Parser.ParseFrom(StripTopLevelField(bytes, 2));
|
||||
EnsureEditablePersistenceVersion(msg);
|
||||
msg.RoomRoleData = BuildDefaultRoomRoleData();
|
||||
return msg.ToByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to a minimal valid blob — never let a missing/locked
|
||||
// file take down room loads entirely.
|
||||
}
|
||||
return BuildMinimalDefaultBlob();
|
||||
}
|
||||
|
||||
private static byte[] StripTopLevelField(byte[] input, int fieldNumberToStrip)
|
||||
{
|
||||
using var output = new MemoryStream(input.Length);
|
||||
var pos = 0;
|
||||
while (pos < input.Length)
|
||||
{
|
||||
var fieldStart = pos;
|
||||
var tag = ReadVarint(input, ref pos);
|
||||
var fieldNumber = (int)(tag >> 3);
|
||||
var wireType = (int)(tag & 0x07);
|
||||
SkipField(input, ref pos, wireType);
|
||||
if (fieldNumber != fieldNumberToStrip)
|
||||
output.Write(input, fieldStart, pos - fieldStart);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static ulong ReadVarint(byte[] input, ref int pos)
|
||||
{
|
||||
ulong value = 0;
|
||||
var shift = 0;
|
||||
while (pos < input.Length)
|
||||
{
|
||||
var b = input[pos++];
|
||||
value |= (ulong)(b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0) return value;
|
||||
shift += 7;
|
||||
if (shift > 63) throw new InvalidDataException("Invalid protobuf varint.");
|
||||
}
|
||||
throw new EndOfStreamException("Unexpected end of protobuf varint.");
|
||||
}
|
||||
|
||||
private static void SkipField(byte[] input, ref int pos, int wireType)
|
||||
{
|
||||
switch (wireType)
|
||||
{
|
||||
case 0:
|
||||
_ = ReadVarint(input, ref pos);
|
||||
break;
|
||||
case 1:
|
||||
pos += 8;
|
||||
break;
|
||||
case 2:
|
||||
pos += checked((int)ReadVarint(input, ref pos));
|
||||
break;
|
||||
case 5:
|
||||
pos += 4;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidDataException($"Unsupported protobuf wire type {wireType}.");
|
||||
}
|
||||
|
||||
if (pos > input.Length) throw new EndOfStreamException("Unexpected end of protobuf field.");
|
||||
}
|
||||
|
||||
public byte[] OverlayAllPermsRoleData(byte[] existingBlob)
|
||||
{
|
||||
var msg = PersistedRoomData.Parser.ParseFrom(existingBlob);
|
||||
@@ -66,14 +142,39 @@ public class RoomDataBlobService
|
||||
return msg.ToByteArray();
|
||||
}
|
||||
|
||||
private static byte[] BuildMinimalDefaultBlob()
|
||||
{
|
||||
var msg = new PersistedRoomData
|
||||
{
|
||||
DEPRECATEDVersion = December2020PersistenceVersion,
|
||||
RoomRoleData = BuildDefaultRoomRoleData(),
|
||||
};
|
||||
|
||||
return msg.ToByteArray();
|
||||
}
|
||||
|
||||
private static RoomRoleCollectionData BuildAllPermsRoleData()
|
||||
{
|
||||
var collection = new RoomRoleCollectionData();
|
||||
var collection = new RoomRoleCollectionData
|
||||
{
|
||||
RecNetMigrationVersion = 1,
|
||||
};
|
||||
collection.RoomRoles.Add(BuildPermissiveRole(0, 100, "Creator"));
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
private static RoomRoleCollectionData BuildDefaultRoomRoleData()
|
||||
{
|
||||
var collection = new RoomRoleCollectionData
|
||||
{
|
||||
RecNetMigrationVersion = 1,
|
||||
};
|
||||
collection.RoomRoles.Add(BuildPermissiveRole(2_097_152, 100, "Creator")); // AG_CREATOR
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
private static byte[] BuildRroEditableBlob()
|
||||
{
|
||||
var msg = new PersistedRoomData
|
||||
@@ -87,7 +188,10 @@ public class RoomDataBlobService
|
||||
|
||||
private static RoomRoleCollectionData BuildRroEditableRoleData()
|
||||
{
|
||||
var collection = new RoomRoleCollectionData();
|
||||
var collection = new RoomRoleCollectionData
|
||||
{
|
||||
RecNetMigrationVersion = 1,
|
||||
};
|
||||
collection.RoomRoles.Add(BuildPermissiveRole(2_097_152, 100, "Creator")); // AG_CREATOR
|
||||
collection.RoomRoles.Add(BuildPermissiveRole(4_194_304, 90, "Co-owner")); // AG_COOWNER
|
||||
collection.RoomRoles.Add(BuildPermissiveRole(8_388_608, 80, "Host")); // AG_HOST
|
||||
@@ -115,7 +219,7 @@ public class RoomDataBlobService
|
||||
DEPRECATEDIsRoleActive = true,
|
||||
DEPRECATEDIsAgRole = true,
|
||||
RoleName = name,
|
||||
RoleVersion = 1,
|
||||
RoleVersion = 20,
|
||||
RoleGuid = StableRoleGuid(roleId),
|
||||
|
||||
// Display name override — keeps the watch's role-list UI from
|
||||
@@ -133,6 +237,7 @@ public class RoomDataBlobService
|
||||
// perms is false.
|
||||
CanAssignRoles = OverrideTrue(),
|
||||
CanInvite = OverrideTrue(),
|
||||
DEPRECATEDCanEditCircuits = OverrideTrue(),
|
||||
CanStartGames = OverrideTrue(),
|
||||
CanTalk = OverrideTrue(),
|
||||
CanPrintPhotos = OverrideTrue(),
|
||||
@@ -145,6 +250,10 @@ public class RoomDataBlobService
|
||||
CanSaveInventions = OverrideTrue(),
|
||||
DisableMicAutoMute = OverrideTrue(),
|
||||
CanUseShareCam = OverrideTrue(),
|
||||
UnknownPermission20 = OverrideTrue(),
|
||||
UnknownPermission21 = OverrideTrue(),
|
||||
UnknownPermission22 = OverrideTrue(),
|
||||
UnknownPermission23 = OverrideTrue(),
|
||||
|
||||
// Vote-kick permission — int field, set to a permissive value.
|
||||
// 0 = AnyoneCanVoteKickAnyone in the 2020 enum (verified via
|
||||
@@ -168,7 +277,15 @@ public class RoomDataBlobService
|
||||
}
|
||||
|
||||
private static string StableRoleGuid(int roleId) =>
|
||||
GuidUtility.Create(GuidUtility.UrlNamespace, $"dorknet-room-role:{roleId}").ToString();
|
||||
roleId switch
|
||||
{
|
||||
2_097_152 => "D8B12451-23C7-4B1D-B573-8C3717A47915",
|
||||
4_194_304 => "88EAECE9-D885-4568-BC96-AF316AD56663",
|
||||
8_388_608 => "BD0F2F3A-F931-419E-B50C-ECDFF3F56B52",
|
||||
16_777_216 => "32300035-3BEA-457E-95C0-1630AFDFA6BD",
|
||||
0 => Guid.Empty.ToString(),
|
||||
_ => GuidUtility.Create(GuidUtility.UrlNamespace, $"dorknet-room-role:{roleId}").ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
internal static class GuidUtility
|
||||
|
||||
Reference in New Issue
Block a user