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:
Alexa
2026-09-27 23:45:57 +03:00
parent 3edeaa5484
commit 806b160acc
4 changed files with 131 additions and 0 deletions
+42
View File
@@ -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()
{