mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
fix(cdn): serve baked RRO rooms a loadable stub blob (version 16, save skeleton)
An enabled baked room still had no Maker Pen: the client fetched the CDN's
synthetic stub for room_{id}_dorknet_v8.dat and silently loaded the room
with no room roles. Measured against Rec Center's working room_100_v1.dat
(10,930 bytes, the same client loads it with Maker Pen), the old 1,872-byte
roles-only stub was stamped PersistedRoomVersion 19 — past the March-2023
client's ceiling of 16, which only the S3-hit path clamped — and lacked the
skeleton every real save carries (last_save_time, activity_id = the room's
LocationReplicationId, connectable graph, scene settings, object model,
room_mood_* blocks).
RoomDataBlobService.BuildRroEditableBlob now emits that skeleton per room
at version 16 (untyped fields appended on the wire; payloads are Rec
Center's defaults verbatim). CdnController resolves the RRO room for the
miss path, passes its location id, and clamps synthesised blobs the same
way it clamps S3 hits. Tests pin version 16, the activity id and
CanUseMakerPen on every role.
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DorkNet.Server.Data;
|
||||
using DorkNet.Server.Data.Entities;
|
||||
using DorkNet.Server.Services;
|
||||
|
||||
namespace DorkNet.Server.Controllers.Cdn;
|
||||
@@ -557,11 +558,18 @@ public class CdnController(
|
||||
return new FileContentResult(TransparentPng, "image/png");
|
||||
}
|
||||
|
||||
if (await ShouldOverlayRroRoomRoleDataAsync(fileName))
|
||||
var rroRoom = await ResolveRroRoomAsync(fileName);
|
||||
if (rroRoom is not null)
|
||||
{
|
||||
logger.LogInformation("[cdn] MISS host={Host} file={File} -> RRO editable role blob",
|
||||
Request.Host.Host, fileName);
|
||||
var rroBlob = roomDataBlob.GetRroEditableBlob();
|
||||
// Per-room stub: a real save's skeleton at the 2023 version
|
||||
// ceiling with the room's own activity_id. The old generic stub
|
||||
// (version 19, roles only) was fetched and silently rejected by
|
||||
// the client, which is why an enabled baked room still had no
|
||||
// Maker Pen. See RoomDataBlobService.BuildRroEditableBlob.
|
||||
logger.LogInformation("[cdn] MISS host={Host} file={File} -> RRO editable blob (room {Room}, activity {Activity})",
|
||||
Request.Host.Host, fileName, rroRoom.Id, rroRoom.LocationReplicationId);
|
||||
var rroBlob = roomDataBlob.GetRroEditableBlob(rroRoom.LocationReplicationId);
|
||||
rroBlob = ClampForClientUnlessDisabled(rroBlob, fileName);
|
||||
Response.Headers.CacheControl = "public, max-age=60";
|
||||
signatures.AddContentSignature(Response, rroBlob);
|
||||
return new FileContentResult(rroBlob, "application/octet-stream");
|
||||
@@ -569,14 +577,34 @@ public class CdnController(
|
||||
|
||||
logger.LogInformation("[cdn] MISS host={Host} file={File} -> default blob",
|
||||
Request.Host.Host, fileName);
|
||||
var fallbackBlob = roomDataBlob.GetDefaultBlob();
|
||||
var fallbackBlob = ClampForClientUnlessDisabled(roomDataBlob.GetDefaultBlob(), fileName);
|
||||
signatures.AddContentSignature(Response, fallbackBlob);
|
||||
return new FileContentResult(fallbackBlob, "application/octet-stream");
|
||||
}
|
||||
|
||||
private async Task<bool> ShouldOverlayRroRoomRoleDataAsync(string fileName)
|
||||
/// <summary>Same 2023 version clamp the S3-hit path applies, for the
|
||||
/// synthesised blobs on the miss path — a stub past the client's ceiling
|
||||
/// is rejected exactly like a modern export would be.</summary>
|
||||
private byte[] ClampForClientUnlessDisabled(byte[] bytes, string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName)) return false;
|
||||
if (!IsRoomDataName(fileName)) return bytes;
|
||||
if (serverSettings.IsRoomBlobVersionClampDisabledAsync().GetAwaiter().GetResult()) return bytes;
|
||||
var (clamped, changed) = RoomDataBlobService.ClampVersionsFor2023(bytes);
|
||||
if (changed)
|
||||
logger.LogInformation("[cdn] clamped synthesised blob versions to 2023 maxima file={File}", fileName);
|
||||
return clamped;
|
||||
}
|
||||
|
||||
private async Task<bool> ShouldOverlayRroRoomRoleDataAsync(string fileName) =>
|
||||
await ResolveRroRoomAsync(fileName) is not null;
|
||||
|
||||
/// <summary>The AG/RRO room a blob name belongs to, or null when the name
|
||||
/// is not an RRO room's (current blob, synthetic <c>room_{id}_*</c> form,
|
||||
/// or a sub-room scene blob). Drives both the role overlay on S3 hits and
|
||||
/// the per-room stub on misses.</summary>
|
||||
private async Task<RoomEntity?> ResolveRroRoomAsync(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName)) return null;
|
||||
|
||||
// MakerPen across ALL Rec Room Originals: overlay the permissive
|
||||
// room-role data (which grants CanUseMakerPen etc.) onto any AG/RRO
|
||||
@@ -587,9 +615,9 @@ public class CdnController(
|
||||
// blob's RoomRoleData field (RoomDataBlobService.OverlayRroEditable-
|
||||
// RoleData) — the game-rule circuit data and GameRoleData are left
|
||||
// intact — so quest rooms keep working, exactly as Stunt Runner does.
|
||||
if (await db.Rooms.AsNoTracking().AnyAsync(r =>
|
||||
r.IsAGRoom && r.CurrentDataBlobName == fileName))
|
||||
return true;
|
||||
var byCurrentBlob = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(r =>
|
||||
r.IsAGRoom && r.CurrentDataBlobName == fileName);
|
||||
if (byCurrentBlob is not null) return byCurrentBlob;
|
||||
|
||||
// Synthetic/default room-data blob names for rooms with no saved blob:
|
||||
// the CURRENT form is room_{id}_dorknet_v8.dat
|
||||
@@ -603,18 +631,19 @@ public class CdnController(
|
||||
{
|
||||
var rest = fileName["room_".Length..];
|
||||
var underscore = rest.IndexOf('_');
|
||||
if (underscore > 0 &&
|
||||
long.TryParse(rest[..underscore], out var roomId) &&
|
||||
await db.Rooms.AsNoTracking().AnyAsync(r => r.Id == roomId && r.IsAGRoom))
|
||||
return true;
|
||||
if (underscore > 0 && long.TryParse(rest[..underscore], out var roomId))
|
||||
{
|
||||
var byId = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(r => r.Id == roomId && r.IsAGRoom);
|
||||
if (byId is not null) return byId;
|
||||
}
|
||||
}
|
||||
|
||||
return await (
|
||||
from scene in db.RoomScenes.AsNoTracking()
|
||||
join room in db.Rooms.AsNoTracking() on scene.RoomId equals room.Id
|
||||
where room.IsAGRoom && scene.DataBlobName == fileName
|
||||
select scene.Id
|
||||
).AnyAsync();
|
||||
select room
|
||||
).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user