mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat(cdn): Rec Royale stub blob ships the room's game-rule chip
Frontier has no baked *_GameRuleWrapperCircuit; real Rec Room delivered Rec Royale's rule chip in the room blob, and without it there is no BattleRoyaleManager and no match. A newer-client export of a custom Rec Royale room shows the saved RecRoyaleSquads chip is minimal — prefab id, palette tag (prefab name lower-cased, 25 chars), circuit node id and an EMPTY GameRuleWrapperData (field 41) — so the stub can generate it. Prefab ids read from the client's spawnable-tool table in resources.assets (SpawnableTool_Runtime_<ENUM> → 20-byte hash → two u32 → 16-byte id), the Squads id cross-checked against the real save: Solos 983db81a…, Squads 857f7500…. The stub for a room whose name contains RecRoyaleSolos / RecRoyaleSquads carries the matching chip as a fourth persistence view. Test: Rec_royale_stub_carries_its_game_rule_chip (both rooms).
This commit is contained in:
@@ -197,6 +197,48 @@ public sealed class RroRoomBlobTests : IClassFixture<DorkNetServerFactory>
|
||||
Assert.Equal(string.Empty, SubRoomZero(await GetJsonAsync(gameClient, $"/rooms/{roomId}")).GetProperty("DataBlob").GetString());
|
||||
}
|
||||
|
||||
/// <summary>Rec Royale's Frontier scene has no baked game rule; real Rec
|
||||
/// Room delivered the RecRoyaleSolos/Squads_GameRuleWrapperCircuit chip
|
||||
/// in the blob. The stub for those rooms must carry it (a fourth
|
||||
/// persistence view with the palette tag the client generates).</summary>
|
||||
[Theory]
|
||||
[InlineData("RecRoyaleSolos", "recroyalesolosgamerulecir")]
|
||||
[InlineData("RecRoyaleSquads", "recroyalesquadsgameruleci")]
|
||||
public async Task Rec_royale_stub_carries_its_game_rule_chip(string roomNamePrefix, string expectedTag)
|
||||
{
|
||||
using var setup = Client("rooms");
|
||||
var admin = await GameClientSessionFactory.CreateAsync(setup, _factory.ApexDomain);
|
||||
|
||||
var roomId = 9_680_000 + Random.Shared.Next(1, 99_999);
|
||||
var roomName = $"{roomNamePrefix}-{Guid.NewGuid():N}"[..24];
|
||||
await using (var scope = _factory.Services.CreateAsyncScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||
(await db.Players.FirstAsync(p => p.Id == admin.PlayerId)).IsAdmin = true;
|
||||
db.Rooms.Add(new RoomEntity
|
||||
{
|
||||
Id = roomId,
|
||||
Name = roomName,
|
||||
CreatorPlayerId = 1,
|
||||
IsAGRoom = true,
|
||||
TagsCsv = "recroomoriginal",
|
||||
LocationReplicationId = "253fa009-6e65-4c90-91a1-7137a56a267f",
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var adminClient = Client("admin", admin);
|
||||
using var cdnClient = Client("cdn");
|
||||
await PostJsonAsync(adminClient, $"/api/admin/v1/rooms/{roomId}/subrooms/0/blob/enable");
|
||||
|
||||
var synthetic = RoomService.SyntheticDefaultRoomDataBlobName(roomId);
|
||||
using var blobResponse = await cdnClient.GetAsync($"/room/{synthetic}");
|
||||
Assert.True(blobResponse.IsSuccessStatusCode);
|
||||
var blobBytes = await blobResponse.Content.ReadAsByteArrayAsync();
|
||||
Assert.Equal(4, CountTopLevelPersistenceViews(blobBytes)); // 3 roles + the rule chip
|
||||
Assert.Contains(expectedTag, System.Text.Encoding.UTF8.GetString(blobBytes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Enable_on_a_saved_subroom_keeps_the_saved_blob()
|
||||
{
|
||||
|
||||
@@ -347,6 +347,19 @@ public class RoomDataBlobService
|
||||
AppendLengthDelimited(output, 2, BuildGameRoleView(seed, "Eliminated", EliminatedGameRoleGuid, nodeId: 11,
|
||||
transform: "0a0f0deac98341152c798b3f1da05e8dc02d0000803f32050d0000803f",
|
||||
roleId: 4_194_304, roleRank: 1015, isDefaultRole: false));
|
||||
|
||||
// 2: the room's GAME-RULE chip, for baked originals whose rule is not
|
||||
// in the scene. Rec Royale's Frontier scene has no baked
|
||||
// *_GameRuleWrapperCircuit (the paintball scenes do); real Rec Room
|
||||
// delivered it in the blob. A saved RecRoyaleSquads chip (custom
|
||||
// Rec Royale room export) is just header + prefab id + palette tag +
|
||||
// node id + an EMPTY GameRuleWrapperData (field 41), so it can be
|
||||
// generated. Prefab ids come from the client's spawnable-tool table
|
||||
// in resources.assets (SpawnableTool_Runtime_<ENUM> → 16-byte id),
|
||||
// cross-checked against ids seen in real saves.
|
||||
var chip = GameRuleChipFor(roomName);
|
||||
if (chip is not null)
|
||||
AppendLengthDelimited(output, 2, BuildGameRuleChipView(seed, chip.Value.PrefabId, chip.Value.Tag, nodeId: 3));
|
||||
// 21: connectable_graph_data { root_node { slot_index=0-ish, field 7 = 1 } }
|
||||
AppendLengthDelimited(output, 21, new byte[] { 0x0A, 0x04, 0x0A, 0x00, 0x38, 0x01 });
|
||||
// 24: scene_settings_data { enforce_host_only_areas=true, use_new_materials=true }
|
||||
@@ -369,6 +382,60 @@ public class RoomDataBlobService
|
||||
output.Write(payload, 0, payload.Length);
|
||||
}
|
||||
|
||||
/// <summary>Game-rule wrapper chips for baked originals whose rule is not
|
||||
/// baked into the scene, keyed by (a substring of) the room name. Ids are
|
||||
/// the client's <c>SpawnableTool_Runtime_*</c> prefab ids (2023-03-21
|
||||
/// resources.assets); the Squads id also matches a real newer-client
|
||||
/// save. Tags are what the client generates: prefab name lower-cased,
|
||||
/// cut to 25 characters.</summary>
|
||||
private static (byte[] PrefabId, string Tag)? GameRuleChipFor(string roomName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(roomName)) return null;
|
||||
var n = roomName.Replace("-", "").Replace("_", "").Replace(" ", "");
|
||||
if (n.Contains("RecRoyaleSquads", StringComparison.OrdinalIgnoreCase))
|
||||
return (Convert.FromHexString("857f750016ca77408ae9f72f9877748a"), "recroyalesquadsgameruleci");
|
||||
if (n.Contains("RecRoyaleSolos", StringComparison.OrdinalIgnoreCase))
|
||||
return (Convert.FromHexString("983db81a68927f419edb0daf1243e93e"), "recroyalesolosgamerulecir");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>PersistenceViewData for a GameRuleCircuitWrapperTool chip,
|
||||
/// laid out like the saved RecRoyaleSquads chip: id, empty
|
||||
/// tool_entity_data, transform, spawnable_tool_data{prefab_id},
|
||||
/// creation_object_data{is_frozen,is_grabbable}, tagged_tool_data{tag},
|
||||
/// empty tool_cleanup_data, circuit_node_data{id}, empty
|
||||
/// game_rule_wrapper_data (field 41).</summary>
|
||||
private static byte[] BuildGameRuleChipView(string seed, byte[] prefabId, string tag, int nodeId)
|
||||
{
|
||||
using var view = new MemoryStream(256);
|
||||
AppendLengthDelimited(view, 1, System.Security.Cryptography.MD5.HashData(
|
||||
System.Text.Encoding.UTF8.GetBytes($"dorknet-rro-gamerule:{seed}:{tag}")));
|
||||
AppendLengthDelimited(view, 9, Array.Empty<byte>());
|
||||
// Same spot the saved Squads chip sat at (position/scale/rotation).
|
||||
AppendLengthDelimited(view, 10, Convert.FromHexString("0a0f0d804081c21520b58b431d941d50c42d0000803f32140d7cbd5d3f150034353a1d75deffbe250034353a"));
|
||||
using (var spawnable = new MemoryStream(20))
|
||||
{
|
||||
AppendLengthDelimited(spawnable, 1, prefabId);
|
||||
AppendLengthDelimited(view, 11, spawnable.ToArray());
|
||||
}
|
||||
AppendLengthDelimited(view, 14, new byte[] { 0x18, 0x01, 0x30, 0x01 });
|
||||
using (var tagged = new MemoryStream(40))
|
||||
{
|
||||
using var tagData = new MemoryStream(32);
|
||||
AppendLengthDelimited(tagData, 1, System.Text.Encoding.UTF8.GetBytes(tag));
|
||||
AppendLengthDelimited(tagged, 1, tagData.ToArray());
|
||||
AppendLengthDelimited(view, 15, tagged.ToArray());
|
||||
}
|
||||
AppendLengthDelimited(view, 22, Array.Empty<byte>());
|
||||
using (var node = new MemoryStream(4))
|
||||
{
|
||||
AppendVarint(node, 1, (ulong)nodeId);
|
||||
AppendLengthDelimited(view, 25, node.ToArray());
|
||||
}
|
||||
AppendLengthDelimited(view, 41, Array.Empty<byte>());
|
||||
return view.ToArray();
|
||||
}
|
||||
|
||||
private static void AppendVarint(Stream output, int fieldNumber, ulong value)
|
||||
{
|
||||
WriteVarint(output, ((ulong)fieldNumber << 3) | 0);
|
||||
|
||||
@@ -232,6 +232,12 @@ actions:
|
||||
the second reason "enable + co-owner" still showed no Maker Pen. The
|
||||
default role sets `can_use_maker_pen` to true, matching the permissive
|
||||
room-role overlay; the built-in roles mirror the reference saves.
|
||||
For the two Rec Royale rooms the stub also carries the room's
|
||||
**game-rule chip** (`RecRoyaleSolos_GameRuleWrapperCircuit` prefab id
|
||||
`983db81a…`, `RecRoyaleSquads_…` `857f7500…`, read from the client's
|
||||
spawnable-tool table; a saved Squads chip is header + prefab id + tag +
|
||||
node id + an empty wrapper payload), so the match logic exists on first
|
||||
join without anyone spawning it by hand.
|
||||
- **Reset to baked scene** — `POST api/admin/v1/rooms/{id}/subrooms/{subRoomId}/blob/reset`.
|
||||
Clears both pointers. This is the escape hatch if a save left the room
|
||||
unplayable. Saved blobs stay in `RoomDataBlobs`, so the in-game restore
|
||||
|
||||
@@ -174,6 +174,22 @@ Stunt Runner: "StuntRunner"; GUID `c48bac02-…` in both), "In-Game"
|
||||
a blob without these leaves a co-owner with no Maker Pen. The stub now
|
||||
generates Rec Royale's own set (view ids derived from the activity id).
|
||||
|
||||
Game-rule chip. A newer-client export of a custom Rec Royale room showed
|
||||
the saved `RecRoyaleSquads_GameRuleWrapperCircuit` chip is minimal:
|
||||
`spawnable_tool_data.prefab_id` `857f7500…`, `tagged_tool_data` tag
|
||||
`recroyalesquadsgameruleci` (prefab name lower-cased, 25 chars),
|
||||
`circuit_node_data.id`, and an EMPTY `game_rule_wrapper_data` (field 41).
|
||||
Prefab ids live in the client's spawnable-tool table in
|
||||
`resources.assets`: after `SpawnableTool_Runtime_<ENUM>` comes a 20-byte
|
||||
hash (len-prefixed), two u32s, then the 16-byte prefab id (len-prefixed),
|
||||
then the enum again, display name and tag. Extracted:
|
||||
Solos `983db81a68927f419edb0daf1243e93e`, Squads
|
||||
`857f750016ca77408ae9f72f9877748a` (matches the save), StuntRunner
|
||||
`28276d92470ff34ab05f72b2707a04d1`. Stunt Runner's other rule-ish object
|
||||
(`84eb8434…`, `game_configuration_data` field 40) is the generic Game
|
||||
Configuration chip, optional — `GameConfigurationTool` falls back to its
|
||||
baked asset. The stub emits the wrapper chip for the two Rec Royale rooms.
|
||||
|
||||
Admin switch: `POST api/admin/v1/rooms/{id}/subrooms/{sub}/blob/enable`
|
||||
stamps `RoomService.SyntheticDefaultRoomDataBlobName(id)` on the scene and
|
||||
room; `.../blob/reset` clears it. See `docs/admin.md` → "RRO room data
|
||||
|
||||
Reference in New Issue
Block a user