mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: lift the CircuitsV2 limits, Rec Royale night mode, RRO room blobs
CircuitsV2 limits are served as UGC.CircuitsV2.Limit.* gameconfig keys and are editable from admin Settings. Retail drops any send over 5 RPCs/frame or 1024 bytes/60 frames; both network budgets now default to 1,000,000,000. The creation limits are raised too, each against the hazard the client binary shows for it: - Chips 3000 -> 30000: the chip-culling manager preallocates four List<Chip>(Chips), so the 1M clamp stays. - Compute 50000 -> 500000: a per-frame budget with no allocation; the 10M cap keeps its counters from wrapping. - CloudValues 100 -> 1000, InkPerChip 15 -> 0: InkPerChip is an ink COST, multiplied by min(chips, CloudValues), so lowering it is the increase. The clamps tighten to 10000 / 30000 to keep that product from overflowing (the room ink cap is a hard-coded 30000). - UnculledChips / VisibleChips 500 -> 3000: render-cost counts only. Room save uploads may now be up to 100 MB (Kestrel's 30 MB default would 413 a big circuits room). Contract and evidence: docs/recroom-2023-circuitsv2-limits.md. Also includes the Rec Royale night mode client feature (docs/recroom-2023-scene-moods.md) and the admin blob enable/reset for baked RRO sub-rooms.
This commit is contained in:
@@ -165,6 +165,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
||||
const [description, setDescription] = useState(room.description);
|
||||
const [accessibility, setAccessibility] = useState(room.accessibility);
|
||||
const [cloningAllowed, setCloningAllowed] = useState(room.cloningAllowed);
|
||||
const [isAGRoom, setIsAGRoom] = useState(room.isAGRoom);
|
||||
const [tagsCsv, setTagsCsv] = useState(room.tagsCsv);
|
||||
const [hotScore, setHotScore] = useState(String(room.hotScore));
|
||||
const [imageName, setImageName] = useState(room.imageName);
|
||||
@@ -176,6 +177,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
||||
setDescription(room.description);
|
||||
setAccessibility(room.accessibility);
|
||||
setCloningAllowed(room.cloningAllowed);
|
||||
setIsAGRoom(room.isAGRoom);
|
||||
setTagsCsv(room.tagsCsv);
|
||||
setHotScore(String(room.hotScore));
|
||||
setImageName(room.imageName);
|
||||
@@ -193,6 +195,7 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
||||
Description: description,
|
||||
Accessibility: accessibility,
|
||||
CloningAllowed: cloningAllowed,
|
||||
IsAGRoom: isAGRoom,
|
||||
TagsCsv: tagsCsv,
|
||||
HotScore: Number.isFinite(hot) ? hot : undefined,
|
||||
ImageName: imageName,
|
||||
@@ -283,6 +286,14 @@ function GeneralTab({ room, onSaved }: { room: RoomDetail; onSaved: () => void }
|
||||
/>
|
||||
<span>Cloning allowed</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm" title="What the client reads as IsRRO. On: load the baked scene from the client's assets and consult the client's built-in per-original table (which forbids creation in Rec Royale). Off: treat as a custom room — roles and blob decide. Flip a CLONE of an original off to test editable Rec Royale.">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAGRoom}
|
||||
onChange={e => setIsAGRoom(e.target.checked)}
|
||||
/>
|
||||
<span>Rec Room Original (IsRRO) — baked scene + client creation table</span>
|
||||
</label>
|
||||
<div className="flex gap-2 pt-2 border-t border-ink-800">
|
||||
<button onClick={save} disabled={busy} className="btn-primary text-xs">
|
||||
{busy ? 'Saving…' : 'Save changes'}
|
||||
@@ -563,6 +574,14 @@ interface SubRoom {
|
||||
canMatchmakeInto: boolean;
|
||||
isSandbox: boolean;
|
||||
dataBlobName: string;
|
||||
/** What the client is actually told to download: scene blob, else room
|
||||
* blob, else synthetic default — or '' for a baked Rec Room Original
|
||||
* with no blob enabled (scene geometry only, no Maker Pen / chips). */
|
||||
effectiveBlobName: string;
|
||||
isBakedOriginal: boolean;
|
||||
/** No RoomScenes row exists yet (seeded baked originals); the entry
|
||||
* scene is synthesised on read and created by "Enable Maker Pen blob". */
|
||||
isVirtual?: boolean;
|
||||
}
|
||||
|
||||
// Per-sub-room player caps. Distinct from the room's own MaxCapacity, which is
|
||||
@@ -576,6 +595,60 @@ function SubRoomsTab({ roomId, roomCapacity }: { roomId: number; roomCapacity: n
|
||||
const { data, loading, error, refresh } = useApi<SubRoom[]>(`/rooms/${roomId}/subrooms`);
|
||||
const [edits, setEdits] = useState<Record<number, string>>({});
|
||||
const [saving, setSaving] = useState<number | null>(null);
|
||||
const [blobBusy, setBlobBusy] = useState<number | null>(null);
|
||||
const [resetArmed, setResetArmed] = useState<number | null>(null);
|
||||
|
||||
// Baked Rec Room Originals ship with no data blob, so the client never
|
||||
// runs its room-data path there (no Maker Pen, no CV2 chips, no saves).
|
||||
// "Enable" stamps the synthetic default name the CDN already answers with
|
||||
// the RRO-editable blob; "Reset" clears the pointers back to scene-only.
|
||||
// Attach a real PersistedRoomData save (any client version) to a sub-room:
|
||||
// the server rewrites it for this room (activity id, 2023 version clamp,
|
||||
// drops output log / sub-room id / unknown fields; "basics" keeps only the
|
||||
// game-role + game-rule objects and strips circuits).
|
||||
const [uploadMode, setUploadMode] = useState<'full' | 'basics'>('basics');
|
||||
const uploadBlob = async (sub: SubRoom, file: File) => {
|
||||
setBlobBusy(sub.subRoomId);
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file, file.name);
|
||||
const res = await api<{ dataBlobName: string; summary: { viewsIn: number; viewsKept: number; versionIn: number; versionOut: number } }>(
|
||||
`/rooms/${roomId}/subrooms/${sub.subRoomId}/blob/upload?mode=${uploadMode}`,
|
||||
{ method: 'POST', formData: fd, timeoutMs: 120000 },
|
||||
);
|
||||
toast.push(
|
||||
`Uploaded ${res.dataBlobName}: ${res.summary.viewsKept}/${res.summary.viewsIn} objects kept, version ${res.summary.versionIn} → ${res.summary.versionOut}`,
|
||||
'success',
|
||||
);
|
||||
refresh();
|
||||
} catch (e) {
|
||||
toast.push((e as Error).message, 'error');
|
||||
} finally {
|
||||
setBlobBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
const setBlob = async (sub: SubRoom, action: 'enable' | 'reset') => {
|
||||
setBlobBusy(sub.subRoomId);
|
||||
try {
|
||||
const res = await api<{ effectiveBlobName: string; changed: boolean }>(
|
||||
`/rooms/${roomId}/subrooms/${sub.subRoomId}/blob/${action}`,
|
||||
{ method: 'POST' },
|
||||
);
|
||||
toast.push(
|
||||
action === 'enable'
|
||||
? `${sub.name || `Sub-room ${sub.subRoomId}`} now loads ${res.effectiveBlobName}`
|
||||
: `${sub.name || `Sub-room ${sub.subRoomId}`} reset to the baked scene (no blob)`,
|
||||
'success',
|
||||
);
|
||||
setResetArmed(null);
|
||||
refresh();
|
||||
} catch (e) {
|
||||
toast.push((e as Error).message, 'error');
|
||||
} finally {
|
||||
setBlobBusy(null);
|
||||
}
|
||||
};
|
||||
|
||||
const save = async (sub: SubRoom) => {
|
||||
const raw = edits[sub.subRoomId];
|
||||
@@ -631,8 +704,71 @@ function SubRoomsTab({ roomId, roomCapacity }: { roomId: number; roomCapacity: n
|
||||
{sub.isSandbox && <span className="badge-neutral">Sandbox</span>}
|
||||
{!sub.canMatchmakeInto && <span className="badge-banned">No matchmaking</span>}
|
||||
</div>
|
||||
{sub.dataBlobName && (
|
||||
<div className="text-xs text-ink-500 font-mono truncate mt-0.5">{sub.dataBlobName}</div>
|
||||
<div className="text-xs text-ink-500 font-mono truncate mt-0.5">
|
||||
{sub.effectiveBlobName
|
||||
? sub.effectiveBlobName
|
||||
: sub.isBakedOriginal
|
||||
? 'no data blob — baked scene only (no Maker Pen / chips)'
|
||||
: '(synthetic default blob)'}
|
||||
</div>
|
||||
{sub.isBakedOriginal && (
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1.5">
|
||||
<label className="text-xs text-ink-400">Upload save</label>
|
||||
<select
|
||||
value={uploadMode}
|
||||
onChange={e => setUploadMode(e.target.value as 'full' | 'basics')}
|
||||
className="input text-xs py-0.5 w-40"
|
||||
title="basics = keep only the game-role and game-rule objects and strip circuits (a newer CircuitsV2 graph is rejected by the 2023 client); full = keep every object"
|
||||
>
|
||||
<option value="basics">basics (roles + game rule)</option>
|
||||
<option value="full">full (every object)</option>
|
||||
</select>
|
||||
<input
|
||||
type="file"
|
||||
accept=".binpb,.room,.dat,.bin"
|
||||
disabled={blobBusy === sub.subRoomId}
|
||||
onChange={e => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) uploadBlob(sub, f);
|
||||
e.target.value = '';
|
||||
}}
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{sub.isBakedOriginal && (
|
||||
<div className="flex items-center gap-2 mt-1.5">
|
||||
{!sub.dataBlobName ? (
|
||||
<button
|
||||
onClick={() => setBlob(sub, 'enable')}
|
||||
disabled={blobBusy === sub.subRoomId}
|
||||
className="btn-secondary text-xs"
|
||||
title="Serve a room data blob for this baked sub-room so Maker Pen, CV2 chips and saves work. The first in-game save replaces it with a real upload."
|
||||
>
|
||||
{blobBusy === sub.subRoomId ? 'Enabling…' : 'Enable Maker Pen blob'}
|
||||
</button>
|
||||
) : resetArmed === sub.subRoomId ? (
|
||||
<>
|
||||
<span className="text-xs text-danger">Drops the blob pointer; saved blobs stay restorable.</span>
|
||||
<button
|
||||
onClick={() => setBlob(sub, 'reset')}
|
||||
disabled={blobBusy === sub.subRoomId}
|
||||
className="btn-danger text-xs"
|
||||
>
|
||||
{blobBusy === sub.subRoomId ? 'Resetting…' : 'Confirm reset'}
|
||||
</button>
|
||||
<button onClick={() => setResetArmed(null)} className="btn-secondary text-xs">Cancel</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setResetArmed(sub.subRoomId)}
|
||||
className="btn-secondary text-xs"
|
||||
title="Go back to the baked scene with no data blob (escape hatch if a save broke the room)."
|
||||
>
|
||||
Reset to baked scene
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -647,8 +783,9 @@ function SubRoomsTab({ roomId, roomCapacity }: { roomId: number; roomCapacity: n
|
||||
/>
|
||||
<button
|
||||
onClick={() => save(sub)}
|
||||
disabled={!dirty || saving === sub.subRoomId}
|
||||
disabled={!dirty || saving === sub.subRoomId || sub.isVirtual}
|
||||
className="btn-primary text-xs"
|
||||
title={sub.isVirtual ? 'This room has no sub-room row yet — enable the blob first' : undefined}
|
||||
>
|
||||
{saving === sub.subRoomId ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
|
||||
@@ -139,10 +139,31 @@ interface DiscoveredGameConfigSettings {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface CircuitsV2NetworkLimit {
|
||||
numPlayersForActivation: number;
|
||||
bytes: number;
|
||||
bytesTimeWindowFrame60s: number;
|
||||
rpcs: number;
|
||||
rpcTimeWindowFrame60s: number;
|
||||
}
|
||||
|
||||
interface CircuitsV2LimitSettings {
|
||||
network: CircuitsV2NetworkLimit[];
|
||||
compute: number;
|
||||
chips: number;
|
||||
cloudValues: number;
|
||||
cloudValuesAutosaveSeconds: number;
|
||||
unculledChips: number;
|
||||
visibleChips: number;
|
||||
inkPerChip: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
||||
const [settings, setSettings] = useState<ServerSettings | null>(null);
|
||||
const [weekly, setWeekly] = useState<WeeklyChallengeSettings | null>(null);
|
||||
const [gameConfigs, setGameConfigs] = useState<DiscoveredGameConfigSettings | null>(null);
|
||||
const [circuits, setCircuits] = useState<CircuitsV2LimitSettings | null>(null);
|
||||
const [rewardOptions, setRewardOptions] = useState<RewardOptions>({
|
||||
avatarItems: [],
|
||||
consumables: [],
|
||||
@@ -152,20 +173,23 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [weeklyBusy, setWeeklyBusy] = useState(false);
|
||||
const [gameConfigBusy, setGameConfigBusy] = useState(false);
|
||||
const [circuitsBusy, setCircuitsBusy] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const [s, w, rewards, configs] = await Promise.all([
|
||||
const [s, w, rewards, configs, limits] = await Promise.all([
|
||||
get<ServerSettings>('/settings'),
|
||||
get<WeeklyChallengeSettings>('/settings/weekly-challenges'),
|
||||
get<RewardOptions>('/settings/weekly-challenges/reward-options'),
|
||||
get<DiscoveredGameConfigSettings>('/settings/gameconfigs'),
|
||||
get<CircuitsV2LimitSettings>('/settings/circuits-limits'),
|
||||
]);
|
||||
setSettings(s);
|
||||
setWeekly(w);
|
||||
setRewardOptions(rewards);
|
||||
setGameConfigs(configs);
|
||||
setCircuits(limits);
|
||||
setErr(null);
|
||||
} catch (e) {
|
||||
setErr((e as Error).message);
|
||||
@@ -470,6 +494,39 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const updateCircuits = (patch: Partial<CircuitsV2LimitSettings>) => {
|
||||
setCircuits((current) => current ? { ...current, ...patch } : current);
|
||||
};
|
||||
|
||||
const updateCircuitsNetwork = (index: number, patch: Partial<CircuitsV2NetworkLimit>) => {
|
||||
setCircuits((current) => current
|
||||
? {
|
||||
...current,
|
||||
network: current.network.map((entry, i) => i === index ? { ...entry, ...patch } : entry),
|
||||
}
|
||||
: current);
|
||||
};
|
||||
|
||||
const saveCircuits = async (reset = false) => {
|
||||
if (!circuits && !reset) return;
|
||||
setCircuitsBusy(true);
|
||||
try {
|
||||
const body = reset ? defaultCircuitsLimits() : circuits;
|
||||
const updated = await api<CircuitsV2LimitSettings>('/settings/circuits-limits', {
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
setCircuits(updated);
|
||||
toast.push(
|
||||
reset ? 'CircuitsV2 limits reset.' : 'CircuitsV2 limits saved. Players must rejoin their room.',
|
||||
'success');
|
||||
} catch (e) {
|
||||
toast.push((e as Error).message, 'error');
|
||||
} finally {
|
||||
setCircuitsBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshBtn = (
|
||||
<button onClick={load} className="btn-secondary text-xs" disabled={busy}>
|
||||
<RefreshCw className={busy ? 'animate-spin' : ''} />
|
||||
@@ -665,6 +722,17 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{circuits && (
|
||||
<CircuitsV2LimitsPanel
|
||||
settings={circuits}
|
||||
busy={circuitsBusy}
|
||||
onChange={updateCircuits}
|
||||
onNetworkChange={updateCircuitsNetwork}
|
||||
onSave={() => saveCircuits(false)}
|
||||
onReset={() => saveCircuits(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{weekly && (
|
||||
<div className="card !p-5 max-w-5xl mt-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
@@ -1068,19 +1136,168 @@ function DiscoveredGameConfigsPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function CircuitsV2LimitsPanel({
|
||||
settings,
|
||||
busy,
|
||||
onChange,
|
||||
onNetworkChange,
|
||||
onSave,
|
||||
onReset,
|
||||
}: {
|
||||
settings: CircuitsV2LimitSettings;
|
||||
busy: boolean;
|
||||
onChange: (patch: Partial<CircuitsV2LimitSettings>) => void;
|
||||
onNetworkChange: (index: number, patch: Partial<CircuitsV2NetworkLimit>) => void;
|
||||
onSave: () => void;
|
||||
onReset: () => void;
|
||||
}) {
|
||||
const number = (key: keyof CircuitsV2LimitSettings, value: string) => {
|
||||
const n = Number.parseFloat(value);
|
||||
onChange({ [key]: Number.isFinite(n) ? n : 0 } as Partial<CircuitsV2LimitSettings>);
|
||||
};
|
||||
|
||||
const netNumber = (index: number, key: keyof CircuitsV2NetworkLimit, value: string) => {
|
||||
const n = Number.parseInt(value, 10);
|
||||
onNetworkChange(index, { [key]: Number.isFinite(n) ? n : 0 } as Partial<CircuitsV2NetworkLimit>);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card !p-5 max-w-5xl mt-5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-sm font-semibold text-ink-50">CircuitsV2 limits</h2>
|
||||
<p className="mt-1 text-xs text-ink-400">
|
||||
Served as the <span className="font-mono">UGC.CircuitsV2.Limit.*</span> gameconfig keys.
|
||||
Retail budgets a circuit 5 RPCs per frame and 1024 bytes per second and silently drops
|
||||
everything past that; DorkNet ships both effectively unlimited, and raises the room-size
|
||||
limits below about 10x past retail. Clients read these once
|
||||
when circuits start, so players must rejoin their room to pick up a change.
|
||||
</p>
|
||||
<p className="mt-2 text-[11px] text-ink-500">
|
||||
Last changed {new Date(settings.updatedAt).toLocaleString()}.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button onClick={onReset} disabled={busy} className="btn-secondary text-xs">
|
||||
<RefreshCw />
|
||||
Defaults
|
||||
</button>
|
||||
<button onClick={onSave} disabled={busy} className="btn-primary text-xs">
|
||||
{busy ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4 lg:grid-cols-2">
|
||||
{settings.network.map((entry, index) => (
|
||||
<div key={index} className="rounded-lg border border-ink-800 bg-ink-950/30 p-4 lg:col-span-2">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-ink-400">
|
||||
Network budget{settings.network.length > 1 ? ` — from ${entry.numPlayersForActivation} players` : ''}
|
||||
</h3>
|
||||
<p className="mt-1 text-[11px] text-ink-500">
|
||||
Each budget is spent over a rolling window measured in frames, so a window of 1 means
|
||||
per frame and 60 means roughly per second at 60fps. Widening a window makes the limit
|
||||
stricter, not looser — raise the budget instead.
|
||||
</p>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<NumberConfig
|
||||
label="Rpcs"
|
||||
value={entry.rpcs}
|
||||
onChange={(v) => netNumber(index, 'rpcs', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="RpcTimeWindowFrame60s"
|
||||
value={entry.rpcTimeWindowFrame60s}
|
||||
onChange={(v) => netNumber(index, 'rpcTimeWindowFrame60s', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="Bytes"
|
||||
value={entry.bytes}
|
||||
onChange={(v) => netNumber(index, 'bytes', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="BytesTimeWindowFrame60s"
|
||||
value={entry.bytesTimeWindowFrame60s}
|
||||
onChange={(v) => netNumber(index, 'bytesTimeWindowFrame60s', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="rounded-lg border border-ink-800 bg-ink-950/30 p-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-ink-400">Circuit budget</h3>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||
<NumberConfig
|
||||
label="Compute"
|
||||
value={settings.compute}
|
||||
hint="Per-frame CPU budget for all circuits in the room. Retail 50000. Keep it finite: it is what stops a runaway loop. Max 10,000,000."
|
||||
onChange={(v) => number('compute', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="Chips"
|
||||
value={settings.chips}
|
||||
hint="Room chip cap. Retail 3000. Max 1,000,000."
|
||||
onChange={(v) => number('chips', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="UnculledChips"
|
||||
value={settings.unculledChips}
|
||||
hint="Chips past this count get culled. Retail 500. Higher costs frame time."
|
||||
onChange={(v) => number('unculledChips', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="VisibleChips"
|
||||
value={settings.visibleChips}
|
||||
hint="Visible-chip limit. Retail 500. Above Chips, adding a chip also charges InkPerChip."
|
||||
onChange={(v) => number('visibleChips', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="InkPerChip"
|
||||
value={settings.inkPerChip}
|
||||
hint="Ink COST per chip against the fixed 30000-ink room cap. Retail 15; 0 = chips use no ink. Max 30,000."
|
||||
onChange={(v) => number('inkPerChip', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-ink-800 bg-ink-950/30 p-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-ink-400">Cloud variables</h3>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||
<NumberConfig
|
||||
label="CloudValues"
|
||||
value={settings.cloudValues}
|
||||
hint="Room cloud-variable cap. Retail 100. Max 10,000."
|
||||
onChange={(v) => number('cloudValues', v)}
|
||||
/>
|
||||
<NumberConfig
|
||||
label="CloudValuesAutosaveSeconds"
|
||||
value={settings.cloudValuesAutosaveSeconds}
|
||||
hint="Seconds between player save-data autosaves. Retail 300. Lower means more save traffic."
|
||||
onChange={(v) => number('cloudValuesAutosaveSeconds', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberConfig({
|
||||
label,
|
||||
value,
|
||||
hint,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
hint?: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<label className="block">
|
||||
<span className="label font-mono normal-case tracking-normal">{label}</span>
|
||||
<input className="input mt-1" type="number" value={value} onChange={(e) => onChange(e.target.value)} />
|
||||
{hint && <span className="mt-1 block text-[11px] text-ink-500">{hint}</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -1128,6 +1345,26 @@ function JsonConfig({
|
||||
);
|
||||
}
|
||||
|
||||
function defaultCircuitsLimits(): CircuitsV2LimitSettings {
|
||||
return {
|
||||
network: [{
|
||||
numPlayersForActivation: 0,
|
||||
bytes: 1_000_000_000,
|
||||
bytesTimeWindowFrame60s: 60,
|
||||
rpcs: 1_000_000_000,
|
||||
rpcTimeWindowFrame60s: 1,
|
||||
}],
|
||||
compute: 500_000,
|
||||
chips: 30_000,
|
||||
cloudValues: 1_000,
|
||||
cloudValuesAutosaveSeconds: 300,
|
||||
unculledChips: 3_000,
|
||||
visibleChips: 3_000,
|
||||
inkPerChip: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function defaultGameConfigs(): DiscoveredGameConfigSettings {
|
||||
return {
|
||||
friendsPostGamePromptUnderFriendCount: 3,
|
||||
|
||||
Reference in New Issue
Block a user