Import december-2020-12-18 from private repo (scrubbed 2026-05-27)

Targets Rec Room 2020.12.18 client.

Re-import after adding:
- DorkNet.ClientMod/dorknet-clientmod.json.template — config schema
  the launcher's manifest-driven patcher renders into UserData/.
- tools/build-patcher-zip.ps1 — produces the dorknet-clientpatch-{key}.zip
  Release asset the launcher fetches.
- .github/workflows/release.yml — auto-publishes both Release assets
  on v1-december-* tag push.

Scrubbed via maintainer-side scrub-for-public.ps1: same drop rules as
the prior import, plus build-patcher-zip.ps1 added to the tools/
allow-list so it survives the scrub.
This commit is contained in:
Alexa
2026-05-27 01:04:12 +03:00
commit 4edf25b2d5
375 changed files with 201291 additions and 0 deletions
@@ -0,0 +1,503 @@
import { useEffect, useState } from 'react';
import { api, get } from '../lib/api';
import { PageHeader } from '../components/PageHeader';
import { useToast } from '../components/Toast';
import { Plus, RefreshCw, Trash } from '../components/Icons';
interface ServerSettings {
signupsDisabled: boolean;
weeklyChallengesCompletedRequired: boolean;
updatedAt: string;
}
interface WeeklyChallenge {
index: number;
name: string;
config: string;
description: string;
tooltip: string;
}
interface WeeklyChallengeSettings {
completedRequired: boolean;
challenges: WeeklyChallenge[];
reward: WeeklyReward;
updatedAt: string;
}
interface WeeklyReward {
giftDropId: number;
xp: number;
tokens: number;
level: number;
giftContext: number;
giftRarity: number;
avatarItemDesc: string;
consumableItemDesc: string;
equipmentPrefabName: string;
equipmentModificationGuid: string;
}
interface RewardOption {
kind: 'avatar' | 'consumable' | 'equipment';
label: string;
giftDropId: number;
avatarItemDesc: string;
consumableItemDesc: string;
equipmentPrefabName: string;
equipmentModificationGuid: string;
}
interface RewardOptions {
avatarItems: RewardOption[];
consumables: RewardOption[];
equipment: RewardOption[];
}
export function Settings() {
const [settings, setSettings] = useState<ServerSettings | null>(null);
const [weekly, setWeekly] = useState<WeeklyChallengeSettings | null>(null);
const [rewardOptions, setRewardOptions] = useState<RewardOptions>({
avatarItems: [],
consumables: [],
equipment: [],
});
const [err, setErr] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [weeklyBusy, setWeeklyBusy] = useState(false);
const toast = useToast();
const load = async () => {
try {
const [s, w, rewards] = await Promise.all([
get<ServerSettings>('/settings'),
get<WeeklyChallengeSettings>('/settings/weekly-challenges'),
get<RewardOptions>('/settings/weekly-challenges/reward-options'),
]);
setSettings(s);
setWeekly(w);
setRewardOptions(rewards);
setErr(null);
} catch (e) {
setErr((e as Error).message);
}
};
useEffect(() => { void load(); }, []);
const toggleSignups = async () => {
if (!settings) return;
const next = !settings.signupsDisabled;
const verb = next ? 'disable' : 'enable';
if (!confirm(`Really ${verb} new account creation? Existing players keep their access either way.`)) return;
setBusy(true);
try {
const updated = await api<ServerSettings>('/settings/signups', {
method: 'POST',
body: { Disabled: next },
});
setSettings(updated);
toast.push(next ? 'New signups blocked.' : 'New signups allowed.', 'success');
} catch (e) {
toast.push((e as Error).message, 'error');
} finally {
setBusy(false);
}
};
const updateWeeklyChallenge = (index: number, patch: Partial<WeeklyChallenge>) => {
setWeekly((current) => {
if (!current) return current;
return {
...current,
challenges: current.challenges.map((challenge, i) =>
i === index ? { ...challenge, ...patch } : challenge),
};
});
};
const addWeeklyChallenge = () => {
setWeekly((current) => {
if (!current) return current;
return {
...current,
challenges: [
...current.challenges,
{
index: current.challenges.length,
name: 'New weekly challenge',
config: '{}',
description: '',
tooltip: '',
},
],
};
});
};
const removeWeeklyChallenge = (index: number) => {
setWeekly((current) => {
if (!current) return current;
const next = current.challenges.filter((_, i) => i !== index);
return { ...current, challenges: next.map((challenge, i) => ({ ...challenge, index: i })) };
});
};
const updateWeeklyReward = (patch: Partial<WeeklyReward>) => {
setWeekly((current) => current ? {
...current,
reward: { ...current.reward, ...patch },
} : current);
};
const rewardNumber = (key: keyof WeeklyReward, value: string) => {
const n = Number.parseInt(value, 10);
updateWeeklyReward({ [key]: Number.isFinite(n) ? Math.max(0, n) : 0 } as Partial<WeeklyReward>);
};
const allRewardOptions = [
...rewardOptions.avatarItems.map((option, index) => ({ option, value: `avatar:${index}` })),
...rewardOptions.consumables.map((option, index) => ({ option, value: `consumable:${index}` })),
...rewardOptions.equipment.map((option, index) => ({ option, value: `equipment:${index}` })),
];
const selectedRewardValue = weekly
? allRewardOptions.find(({ option }) =>
option.avatarItemDesc === weekly.reward.avatarItemDesc &&
option.consumableItemDesc === weekly.reward.consumableItemDesc &&
option.equipmentPrefabName === weekly.reward.equipmentPrefabName &&
option.equipmentModificationGuid === weekly.reward.equipmentModificationGuid)?.value
?? (
weekly.reward.avatarItemDesc ||
weekly.reward.consumableItemDesc ||
weekly.reward.equipmentPrefabName ||
weekly.reward.equipmentModificationGuid
? 'current'
: 'none'
)
: 'none';
const selectedRewardOption = selectedRewardValue === 'current'
? weekly?.reward
: allRewardOptions.find((entry) => entry.value === selectedRewardValue)?.option ?? null;
const selectWeeklyReward = (value: string) => {
if (value === 'none') {
updateWeeklyReward({
giftDropId: 0,
avatarItemDesc: '',
consumableItemDesc: '',
equipmentPrefabName: '',
equipmentModificationGuid: '',
});
return;
}
const selected = allRewardOptions.find((entry) => entry.value === value)?.option;
if (!selected) return;
updateWeeklyReward({
giftDropId: selected.giftDropId,
avatarItemDesc: selected.avatarItemDesc,
consumableItemDesc: selected.consumableItemDesc,
equipmentPrefabName: selected.equipmentPrefabName,
equipmentModificationGuid: selected.equipmentModificationGuid,
});
};
const resetWeeklyDefaults = async () => {
if (!confirm('Reset weekly challenges to the built-in defaults?')) return;
setWeeklyBusy(true);
try {
const updated = await api<WeeklyChallengeSettings>('/settings/weekly-challenges', {
method: 'POST',
body: { CompletedRequired: true, Challenges: [] },
});
setWeekly(updated);
setSettings((current) => current ? {
...current,
weeklyChallengesCompletedRequired: updated.completedRequired,
updatedAt: updated.updatedAt,
} : current);
toast.push('Weekly challenges reset.', 'success');
} catch (e) {
toast.push((e as Error).message, 'error');
} finally {
setWeeklyBusy(false);
}
};
const saveWeeklyChallenges = async () => {
if (!weekly) return;
setWeeklyBusy(true);
try {
const updated = await api<WeeklyChallengeSettings>('/settings/weekly-challenges', {
method: 'POST',
body: {
CompletedRequired: weekly.completedRequired,
Challenges: weekly.challenges,
Reward: weekly.reward,
},
});
setWeekly(updated);
setSettings((current) => current ? {
...current,
weeklyChallengesCompletedRequired: updated.completedRequired,
updatedAt: updated.updatedAt,
} : current);
toast.push('Weekly challenges saved.', 'success');
} catch (e) {
toast.push((e as Error).message, 'error');
} finally {
setWeeklyBusy(false);
}
};
return (
<div>
<PageHeader
title="Server settings"
blurb="Runtime toggles applied across every replica without a redeploy."
actions={
<button onClick={load} className="btn-secondary text-xs" disabled={busy}>
<RefreshCw className={busy ? 'animate-spin' : ''} />
Refresh
</button>
}
/>
{err && (
<div className="card border-danger/30 bg-danger/5 px-4 py-3 text-sm text-danger mb-4">{err}</div>
)}
{!settings && !weekly && !err && (
<div className="card !p-6 text-center text-xs text-ink-400">Loading…</div>
)}
{settings && (
<div className="card !p-5 max-w-2xl">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<h2 className="text-sm font-semibold text-ink-50">In-game signups</h2>
<p className="mt-1 text-xs text-ink-400">
When disabled, the watch's account-creation flow returns an error to the player instead of minting a new account.
Existing logins keep working — only brand-new signups are blocked.
</p>
<p className="mt-2 text-[11px] text-ink-500">
Last changed {new Date(settings.updatedAt).toLocaleString()}.
</p>
</div>
<button
onClick={toggleSignups}
disabled={busy}
className={(settings.signupsDisabled ? 'btn-primary' : 'btn-danger') + ' text-xs shrink-0'}
>
{busy ? 'Working…' : settings.signupsDisabled ? 'Enable signups' : 'Disable signups'}
</button>
</div>
<div className="mt-4 text-xs">
{settings.signupsDisabled
? <span className="badge-banned">Signups disabled</span>
: <span className="badge-online">Signups allowed</span>}
</div>
</div>
)}
{weekly && (
<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">Weekly challenges</h2>
<p className="mt-1 text-xs text-ink-400">
These are sent to the watch as the current challenge map. Players start each weekly challenge incomplete until their own progress is reported.
</p>
<p className="mt-2 text-[11px] text-ink-500">
Last changed {new Date(weekly.updatedAt).toLocaleString()}.
</p>
</div>
<div className="flex flex-wrap gap-2">
<button onClick={addWeeklyChallenge} disabled={weeklyBusy} className="btn-secondary text-xs">
<Plus />
Add
</button>
<button onClick={resetWeeklyDefaults} disabled={weeklyBusy} className="btn-secondary text-xs">
<RefreshCw />
Defaults
</button>
<button onClick={saveWeeklyChallenges} disabled={weeklyBusy} className="btn-primary text-xs">
{weeklyBusy ? 'Saving…' : 'Save'}
</button>
</div>
</div>
<label className="mt-5 flex items-center gap-3 text-sm text-ink-100">
<input
type="checkbox"
checked={weekly.completedRequired}
onChange={(e) => setWeekly({ ...weekly, completedRequired: e.target.checked })}
className="size-4 rounded border-ink-700 bg-ink-900 text-brand-500 focus:ring-brand-500/30"
/>
CompletedRequired
</label>
<div className="mt-5 rounded-lg border border-ink-800 bg-ink-950/30 p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="text-sm font-semibold text-ink-50">Weekly reward</h3>
<span className="badge-admin">{weekly.reward.xp} XP · {weekly.reward.tokens} tokens</span>
</div>
<div className="grid gap-3 md:grid-cols-3">
<label className="block">
<span className="label">XP</span>
<input
className="input mt-1"
type="number"
min={0}
value={weekly.reward.xp}
onChange={(e) => rewardNumber('xp', e.target.value)}
/>
</label>
<label className="block">
<span className="label">Tokens</span>
<input
className="input mt-1"
type="number"
min={0}
value={weekly.reward.tokens}
onChange={(e) => rewardNumber('tokens', e.target.value)}
/>
</label>
<label className="block">
<span className="label">Gift drop id</span>
<input
className="input mt-1"
type="number"
min={0}
value={weekly.reward.giftDropId}
onChange={(e) => rewardNumber('giftDropId', e.target.value)}
/>
</label>
<label className="block">
<span className="label">Level</span>
<input
className="input mt-1"
type="number"
min={0}
value={weekly.reward.level}
onChange={(e) => rewardNumber('level', e.target.value)}
/>
</label>
<label className="block">
<span className="label">Gift context</span>
<input
className="input mt-1"
type="number"
min={0}
value={weekly.reward.giftContext}
onChange={(e) => rewardNumber('giftContext', e.target.value)}
/>
</label>
<label className="block">
<span className="label">Gift rarity</span>
<input
className="input mt-1"
type="number"
min={0}
value={weekly.reward.giftRarity}
onChange={(e) => rewardNumber('giftRarity', e.target.value)}
/>
</label>
<label className="block md:col-span-3">
<span className="label">Reward item</span>
<select
className="input mt-1"
value={selectedRewardValue}
onChange={(e) => selectWeeklyReward(e.target.value)}
>
<option value="none">None</option>
{selectedRewardValue === 'current' && (
<option value="current">Current configured item</option>
)}
<optgroup label="Avatar items">
{rewardOptions.avatarItems.map((option, index) => (
<option key={`avatar:${index}`} value={`avatar:${index}`}>{option.label}</option>
))}
</optgroup>
<optgroup label="Consumables">
{rewardOptions.consumables.map((option, index) => (
<option key={`consumable:${index}`} value={`consumable:${index}`}>{option.label}</option>
))}
</optgroup>
<optgroup label="Weapon skins">
{rewardOptions.equipment.map((option, index) => (
<option key={`equipment:${index}`} value={`equipment:${index}`}>{option.label}</option>
))}
</optgroup>
</select>
</label>
{selectedRewardOption && selectedRewardValue !== 'none' && (
<div className="md:col-span-3 rounded border border-ink-800 bg-ink-950/40 px-3 py-2 text-[11px] text-ink-400">
<div className="font-mono break-all">
{selectedRewardOption.avatarItemDesc && `avatar: ${selectedRewardOption.avatarItemDesc}`}
{selectedRewardOption.consumableItemDesc && `consumable: ${selectedRewardOption.consumableItemDesc}`}
{selectedRewardOption.equipmentPrefabName && `equipment: ${selectedRewardOption.equipmentPrefabName} / ${selectedRewardOption.equipmentModificationGuid}`}
</div>
</div>
)}
</div>
</div>
<div className="mt-5 grid gap-3">
{weekly.challenges.map((challenge, index) => (
<div key={index} className="rounded-lg border border-ink-800 bg-ink-950/30 p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<span className="badge-neutral">Challenge {index + 1}</span>
<button
onClick={() => removeWeeklyChallenge(index)}
disabled={weeklyBusy || weekly.challenges.length <= 1}
className="btn-ghost !px-2 !py-1 text-danger"
title="Remove challenge"
>
<Trash />
</button>
</div>
<div className="grid gap-3 md:grid-cols-2">
<label className="block">
<span className="label">Name</span>
<input
className="input mt-1"
value={challenge.name}
onChange={(e) => updateWeeklyChallenge(index, { name: e.target.value })}
/>
</label>
<label className="block">
<span className="label">Tooltip</span>
<input
className="input mt-1"
value={challenge.tooltip}
onChange={(e) => updateWeeklyChallenge(index, { tooltip: e.target.value })}
/>
</label>
<label className="block md:col-span-2">
<span className="label">Description</span>
<input
className="input mt-1"
value={challenge.description}
onChange={(e) => updateWeeklyChallenge(index, { description: e.target.value })}
/>
</label>
<label className="block md:col-span-2">
<span className="label">Config</span>
<textarea
className="input mt-1 min-h-24 font-mono text-xs"
value={challenge.config}
onChange={(e) => updateWeeklyChallenge(index, { config: e.target.value })}
/>
</label>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}