mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
Add game configuration management: implement endpoints for retrieving and updating discovered game configurations; enhance database schema and UI integration
This commit is contained in:
@@ -1531,6 +1531,19 @@ public class AdminController(
|
|||||||
return Ok(doors);
|
return Ok(doors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("settings/gameconfigs")]
|
||||||
|
public async Task<ActionResult> GetDiscoveredGameConfigs()
|
||||||
|
=> Ok(await serverSettings.GetDiscoveredGameConfigsAsync());
|
||||||
|
|
||||||
|
[HttpPost("settings/gameconfigs")]
|
||||||
|
public async Task<ActionResult> SetDiscoveredGameConfigs([FromBody] DiscoveredGameConfigSettings body)
|
||||||
|
{
|
||||||
|
var settings = await serverSettings.SetDiscoveredGameConfigsAsync(body);
|
||||||
|
await LogAsync("gameconfigs_updated", "system", 0, "discovered");
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return Ok(settings);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Signup codes ─────────────────────────────────────────────────────
|
// ── Signup codes ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <summary>GET <c>api/admin/v1/signup-codes</c> — every issued code
|
/// <summary>GET <c>api/admin/v1/signup-codes</c> — every issued code
|
||||||
|
|||||||
@@ -611,6 +611,7 @@ public class DorkNetDbContext(DbContextOptions<DorkNetDbContext> options) : DbCo
|
|||||||
e.Property(s => s.WeeklyChallengeRewardJson).HasDefaultValue(string.Empty);
|
e.Property(s => s.WeeklyChallengeRewardJson).HasDefaultValue(string.Empty);
|
||||||
e.Property(s => s.PlayMenuTagsJson).HasDefaultValue(string.Empty);
|
e.Property(s => s.PlayMenuTagsJson).HasDefaultValue(string.Empty);
|
||||||
e.Property(s => s.RecCenterDoorsJson).HasDefaultValue(string.Empty);
|
e.Property(s => s.RecCenterDoorsJson).HasDefaultValue(string.Empty);
|
||||||
|
e.Property(s => s.DiscoveredGameConfigsJson).HasDefaultValue(string.Empty);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<RoomKeyEntity>(e =>
|
modelBuilder.Entity<RoomKeyEntity>(e =>
|
||||||
|
|||||||
@@ -48,5 +48,11 @@ public class ServerSettingsEntity
|
|||||||
/// Door.{Category}.Title and Door.{Category}.Query entries.</summary>
|
/// Door.{Category}.Title and Door.{Category}.Query entries.</summary>
|
||||||
public string RecCenterDoorsJson { get; set; } = string.Empty;
|
public string RecCenterDoorsJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Admin-managed values for GameConfig keys confirmed in
|
||||||
|
/// the 2020.12 decomp. Kept typed in the admin API; stored as JSON
|
||||||
|
/// here so newly discovered built-in keys do not require a table
|
||||||
|
/// migration each time.</summary>
|
||||||
|
public string DiscoveredGameConfigsJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,18 @@ public class ServerSettingsService(DorkNetDbContext db)
|
|||||||
return ToRecCenterDoors(row);
|
return ToRecCenterDoors(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DiscoveredGameConfigSettings> GetDiscoveredGameConfigsAsync()
|
||||||
|
{
|
||||||
|
var row = await GetAsync();
|
||||||
|
return ToDiscoveredGameConfigs(row);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyList<GameConfigurationSetting>> GetGameConfigurationsAsync()
|
public async Task<IReadOnlyList<GameConfigurationSetting>> GetGameConfigurationsAsync()
|
||||||
{
|
{
|
||||||
var doors = await GetRecCenterDoorsAsync();
|
var row = await GetAsync();
|
||||||
return ToGameConfigurations(doors);
|
return ToGameConfigurations(
|
||||||
|
ToRecCenterDoors(row),
|
||||||
|
ToDiscoveredGameConfigs(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PlayMenuTagSettings> SetPlayMenuTagsAsync(
|
public async Task<PlayMenuTagSettings> SetPlayMenuTagsAsync(
|
||||||
@@ -95,6 +103,17 @@ public class ServerSettingsService(DorkNetDbContext db)
|
|||||||
return normalized with { UpdatedAt = existing.UpdatedAt };
|
return normalized with { UpdatedAt = existing.UpdatedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<DiscoveredGameConfigSettings> SetDiscoveredGameConfigsAsync(
|
||||||
|
DiscoveredGameConfigSettings settings)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeDiscoveredGameConfigs(settings);
|
||||||
|
var existing = await GetTrackedRowAsync();
|
||||||
|
existing.DiscoveredGameConfigsJson = JsonSerializer.Serialize(normalized, JsonOptions);
|
||||||
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
return normalized with { UpdatedAt = existing.UpdatedAt };
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WeeklyChallengeSettings> SetWeeklyChallengesAsync(
|
public async Task<WeeklyChallengeSettings> SetWeeklyChallengesAsync(
|
||||||
bool completedRequired,
|
bool completedRequired,
|
||||||
IReadOnlyList<WeeklyChallengeTemplate> challenges,
|
IReadOnlyList<WeeklyChallengeTemplate> challenges,
|
||||||
@@ -209,6 +228,28 @@ public class ServerSettingsService(DorkNetDbContext db)
|
|||||||
return DefaultRecCenterDoors() with { UpdatedAt = row.UpdatedAt };
|
return DefaultRecCenterDoors() with { UpdatedAt = row.UpdatedAt };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DiscoveredGameConfigSettings ToDiscoveredGameConfigs(ServerSettingsEntity row)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(row.DiscoveredGameConfigsJson))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<DiscoveredGameConfigSettings>(
|
||||||
|
row.DiscoveredGameConfigsJson,
|
||||||
|
JsonOptions);
|
||||||
|
if (parsed is not null)
|
||||||
|
return NormalizeDiscoveredGameConfigs(parsed)
|
||||||
|
with { UpdatedAt = row.UpdatedAt };
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Bad settings should not break gameconfig bootstrap.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultDiscoveredGameConfigs() with { UpdatedAt = row.UpdatedAt };
|
||||||
|
}
|
||||||
|
|
||||||
private static List<WeeklyChallengeTemplate> NormalizeWeeklyChallenges(
|
private static List<WeeklyChallengeTemplate> NormalizeWeeklyChallenges(
|
||||||
IReadOnlyList<WeeklyChallengeTemplate> challenges)
|
IReadOnlyList<WeeklyChallengeTemplate> challenges)
|
||||||
{
|
{
|
||||||
@@ -323,8 +364,27 @@ public class ServerSettingsService(DorkNetDbContext db)
|
|||||||
],
|
],
|
||||||
UpdatedAt: DateTime.UtcNow);
|
UpdatedAt: DateTime.UtcNow);
|
||||||
|
|
||||||
|
public static DiscoveredGameConfigSettings DefaultDiscoveredGameConfigs() =>
|
||||||
|
new(
|
||||||
|
FriendsPostGamePromptUnderFriendCount: 3,
|
||||||
|
FriendsSuggestFriendCodeOnFriendsScreenCount: 5,
|
||||||
|
ScreensForceVerification: false,
|
||||||
|
VrForceVerification: false,
|
||||||
|
RewardsUseRewardSelection: false,
|
||||||
|
RewardsSelectionTimeout: 30,
|
||||||
|
RoomDetailsPhotoRollEnabled: false,
|
||||||
|
LoadingNetworkTimeout: 30,
|
||||||
|
RunningNetworkTimeout: 30,
|
||||||
|
SynchronizedFieldRemoveDefaultEntries: false,
|
||||||
|
RenderingDisableSrpBatcher: false,
|
||||||
|
SplitTestSoftOverrides: "{}",
|
||||||
|
SplitTestHardOverrides: "{}",
|
||||||
|
SplitTestSegmentProbabilities: "{}",
|
||||||
|
UpdatedAt: DateTime.UtcNow);
|
||||||
|
|
||||||
public static IReadOnlyList<GameConfigurationSetting> ToGameConfigurations(
|
public static IReadOnlyList<GameConfigurationSetting> ToGameConfigurations(
|
||||||
RecCenterDoorSettings doors)
|
RecCenterDoorSettings doors,
|
||||||
|
DiscoveredGameConfigSettings discovered)
|
||||||
{
|
{
|
||||||
var rows = new List<GameConfigurationSetting>();
|
var rows = new List<GameConfigurationSetting>();
|
||||||
foreach (var door in NormalizeRecCenterDoors(doors.Doors).Doors)
|
foreach (var door in NormalizeRecCenterDoors(doors.Doors).Doors)
|
||||||
@@ -333,16 +393,22 @@ public class ServerSettingsService(DorkNetDbContext db)
|
|||||||
rows.Add(new($"Door.{door.Key}.Query", door.Query));
|
rows.Add(new($"Door.{door.Key}.Query", door.Query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var gameConfig = NormalizeDiscoveredGameConfigs(discovered);
|
||||||
rows.AddRange([
|
rows.AddRange([
|
||||||
new("Rewards.UseRewardSelection", "false"),
|
new("Friends.PostGamePromptUnderFriendCount", gameConfig.FriendsPostGamePromptUnderFriendCount.ToString()),
|
||||||
new("RoomDetails.PhotoRollEnabled", "false"),
|
new("Friends.SuggestFriendCodeOnFriendsScreenCount", gameConfig.FriendsSuggestFriendCodeOnFriendsScreenCount.ToString()),
|
||||||
new("SynchronizedField.RemoveDefaultEntries", "false"),
|
new("Screens.ForceVerification", gameConfig.ScreensForceVerification ? "1" : "0"),
|
||||||
new("Rendering.DisableSrpBatcher", "false"),
|
new("VR.ForceVerification", gameConfig.VrForceVerification ? "1" : "0"),
|
||||||
new("splitTestSoftOverrides", "{}"),
|
new("Rewards.UseRewardSelection", gameConfig.RewardsUseRewardSelection.ToString().ToLowerInvariant()),
|
||||||
new("splitTestHardOverrides", "{}"),
|
new("Rewards.SelectionTimeout", gameConfig.RewardsSelectionTimeout.ToString()),
|
||||||
new("splitTestSegmentProbabilities", "{}"),
|
new("RoomDetails.PhotoRollEnabled", gameConfig.RoomDetailsPhotoRollEnabled.ToString().ToLowerInvariant()),
|
||||||
new("loadingNetworkTimeout", "30"),
|
new("loadingNetworkTimeout", gameConfig.LoadingNetworkTimeout.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
new("runningNetworkTimeout", "30"),
|
new("runningNetworkTimeout", gameConfig.RunningNetworkTimeout.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
new("SynchronizedField.RemoveDefaultEntries", gameConfig.SynchronizedFieldRemoveDefaultEntries.ToString().ToLowerInvariant()),
|
||||||
|
new("Rendering.DisableSrpBatcher", gameConfig.RenderingDisableSrpBatcher.ToString().ToLowerInvariant()),
|
||||||
|
new("splitTestSoftOverrides", gameConfig.SplitTestSoftOverrides),
|
||||||
|
new("splitTestHardOverrides", gameConfig.SplitTestHardOverrides),
|
||||||
|
new("splitTestSegmentProbabilities", gameConfig.SplitTestSegmentProbabilities),
|
||||||
]);
|
]);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
@@ -415,6 +481,41 @@ public class ServerSettingsService(DorkNetDbContext db)
|
|||||||
normalized.Count > 0 ? normalized : DefaultRecCenterDoors().Doors,
|
normalized.Count > 0 ? normalized : DefaultRecCenterDoors().Doors,
|
||||||
DateTime.UtcNow);
|
DateTime.UtcNow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DiscoveredGameConfigSettings NormalizeDiscoveredGameConfigs(
|
||||||
|
DiscoveredGameConfigSettings settings)
|
||||||
|
{
|
||||||
|
static string JsonObjectOrDefault(string value)
|
||||||
|
{
|
||||||
|
value = string.IsNullOrWhiteSpace(value) ? "{}" : value.Trim();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(value);
|
||||||
|
return doc.RootElement.ValueKind == JsonValueKind.Object ? value : "{}";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DiscoveredGameConfigSettings(
|
||||||
|
FriendsPostGamePromptUnderFriendCount: Math.Clamp(settings.FriendsPostGamePromptUnderFriendCount, 0, 100),
|
||||||
|
FriendsSuggestFriendCodeOnFriendsScreenCount: Math.Clamp(settings.FriendsSuggestFriendCodeOnFriendsScreenCount, 0, 100),
|
||||||
|
ScreensForceVerification: settings.ScreensForceVerification,
|
||||||
|
VrForceVerification: settings.VrForceVerification,
|
||||||
|
RewardsUseRewardSelection: settings.RewardsUseRewardSelection,
|
||||||
|
RewardsSelectionTimeout: Math.Clamp(settings.RewardsSelectionTimeout, 0, 300),
|
||||||
|
RoomDetailsPhotoRollEnabled: settings.RoomDetailsPhotoRollEnabled,
|
||||||
|
LoadingNetworkTimeout: Math.Clamp(settings.LoadingNetworkTimeout, 1, 300),
|
||||||
|
RunningNetworkTimeout: Math.Clamp(settings.RunningNetworkTimeout, 1, 300),
|
||||||
|
SynchronizedFieldRemoveDefaultEntries: settings.SynchronizedFieldRemoveDefaultEntries,
|
||||||
|
RenderingDisableSrpBatcher: settings.RenderingDisableSrpBatcher,
|
||||||
|
SplitTestSoftOverrides: JsonObjectOrDefault(settings.SplitTestSoftOverrides),
|
||||||
|
SplitTestHardOverrides: JsonObjectOrDefault(settings.SplitTestHardOverrides),
|
||||||
|
SplitTestSegmentProbabilities: JsonObjectOrDefault(settings.SplitTestSegmentProbabilities),
|
||||||
|
UpdatedAt: DateTime.UtcNow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record WeeklyChallengeSettings(
|
public sealed record WeeklyChallengeSettings(
|
||||||
@@ -461,3 +562,20 @@ public sealed record GameConfigurationSetting(
|
|||||||
string Value,
|
string Value,
|
||||||
DateTime? StartTime = null,
|
DateTime? StartTime = null,
|
||||||
DateTime? EndTime = null);
|
DateTime? EndTime = null);
|
||||||
|
|
||||||
|
public sealed record DiscoveredGameConfigSettings(
|
||||||
|
int FriendsPostGamePromptUnderFriendCount,
|
||||||
|
int FriendsSuggestFriendCodeOnFriendsScreenCount,
|
||||||
|
bool ScreensForceVerification,
|
||||||
|
bool VrForceVerification,
|
||||||
|
bool RewardsUseRewardSelection,
|
||||||
|
int RewardsSelectionTimeout,
|
||||||
|
bool RoomDetailsPhotoRollEnabled,
|
||||||
|
float LoadingNetworkTimeout,
|
||||||
|
float RunningNetworkTimeout,
|
||||||
|
bool SynchronizedFieldRemoveDefaultEntries,
|
||||||
|
bool RenderingDisableSrpBatcher,
|
||||||
|
string SplitTestSoftOverrides,
|
||||||
|
string SplitTestHardOverrides,
|
||||||
|
string SplitTestSegmentProbabilities,
|
||||||
|
DateTime UpdatedAt);
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ public static class DatabaseBootstrap
|
|||||||
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""PlayMenuTagsJson"" text NOT NULL DEFAULT '';");
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""PlayMenuTagsJson"" text NOT NULL DEFAULT '';");
|
||||||
await db.Database.ExecuteSqlRawAsync(
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""RecCenterDoorsJson"" text NOT NULL DEFAULT '';");
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""RecCenterDoorsJson"" text NOT NULL DEFAULT '';");
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN IF NOT EXISTS ""DiscoveredGameConfigsJson"" text NOT NULL DEFAULT '';");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,6 +274,18 @@ public static class DatabaseBootstrap
|
|||||||
// ignore the duplicate-column error; any other schema problem
|
// ignore the duplicate-column error; any other schema problem
|
||||||
// will surface when EF reads ServerSettings.
|
// will surface when EF reads ServerSettings.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await db.Database.ExecuteSqlRawAsync(
|
||||||
|
@"ALTER TABLE ""ServerSettings"" ADD COLUMN ""DiscoveredGameConfigsJson"" TEXT NOT NULL DEFAULT '';");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// SQLite has no ADD COLUMN IF NOT EXISTS. If it already exists,
|
||||||
|
// ignore the duplicate-column error; any other schema problem
|
||||||
|
// will surface when EF reads ServerSettings.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>SQLite migration-history reconciliation. Handles two
|
/// <summary>SQLite migration-history reconciliation. Handles two
|
||||||
|
|||||||
@@ -54,9 +54,28 @@ interface RewardOptions {
|
|||||||
equipment: RewardOption[];
|
equipment: RewardOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DiscoveredGameConfigSettings {
|
||||||
|
friendsPostGamePromptUnderFriendCount: number;
|
||||||
|
friendsSuggestFriendCodeOnFriendsScreenCount: number;
|
||||||
|
screensForceVerification: boolean;
|
||||||
|
vrForceVerification: boolean;
|
||||||
|
rewardsUseRewardSelection: boolean;
|
||||||
|
rewardsSelectionTimeout: number;
|
||||||
|
roomDetailsPhotoRollEnabled: boolean;
|
||||||
|
loadingNetworkTimeout: number;
|
||||||
|
runningNetworkTimeout: number;
|
||||||
|
synchronizedFieldRemoveDefaultEntries: boolean;
|
||||||
|
renderingDisableSrpBatcher: boolean;
|
||||||
|
splitTestSoftOverrides: string;
|
||||||
|
splitTestHardOverrides: string;
|
||||||
|
splitTestSegmentProbabilities: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
||||||
const [settings, setSettings] = useState<ServerSettings | null>(null);
|
const [settings, setSettings] = useState<ServerSettings | null>(null);
|
||||||
const [weekly, setWeekly] = useState<WeeklyChallengeSettings | null>(null);
|
const [weekly, setWeekly] = useState<WeeklyChallengeSettings | null>(null);
|
||||||
|
const [gameConfigs, setGameConfigs] = useState<DiscoveredGameConfigSettings | null>(null);
|
||||||
const [rewardOptions, setRewardOptions] = useState<RewardOptions>({
|
const [rewardOptions, setRewardOptions] = useState<RewardOptions>({
|
||||||
avatarItems: [],
|
avatarItems: [],
|
||||||
consumables: [],
|
consumables: [],
|
||||||
@@ -65,18 +84,21 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
|||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [weeklyBusy, setWeeklyBusy] = useState(false);
|
const [weeklyBusy, setWeeklyBusy] = useState(false);
|
||||||
|
const [gameConfigBusy, setGameConfigBusy] = useState(false);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const [s, w, rewards] = await Promise.all([
|
const [s, w, rewards, configs] = await Promise.all([
|
||||||
get<ServerSettings>('/settings'),
|
get<ServerSettings>('/settings'),
|
||||||
get<WeeklyChallengeSettings>('/settings/weekly-challenges'),
|
get<WeeklyChallengeSettings>('/settings/weekly-challenges'),
|
||||||
get<RewardOptions>('/settings/weekly-challenges/reward-options'),
|
get<RewardOptions>('/settings/weekly-challenges/reward-options'),
|
||||||
|
get<DiscoveredGameConfigSettings>('/settings/gameconfigs'),
|
||||||
]);
|
]);
|
||||||
setSettings(s);
|
setSettings(s);
|
||||||
setWeekly(w);
|
setWeekly(w);
|
||||||
setRewardOptions(rewards);
|
setRewardOptions(rewards);
|
||||||
|
setGameConfigs(configs);
|
||||||
setErr(null);
|
setErr(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr((e as Error).message);
|
setErr((e as Error).message);
|
||||||
@@ -252,6 +274,28 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateGameConfigs = (patch: Partial<DiscoveredGameConfigSettings>) => {
|
||||||
|
setGameConfigs((current) => current ? { ...current, ...patch } : current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveGameConfigs = async (reset = false) => {
|
||||||
|
if (!gameConfigs && !reset) return;
|
||||||
|
setGameConfigBusy(true);
|
||||||
|
try {
|
||||||
|
const body = reset ? defaultGameConfigs() : gameConfigs;
|
||||||
|
const updated = await api<DiscoveredGameConfigSettings>('/settings/gameconfigs', {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
setGameConfigs(updated);
|
||||||
|
toast.push(reset ? 'GameConfig settings reset.' : 'GameConfig settings saved.', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
toast.push((e as Error).message, 'error');
|
||||||
|
} finally {
|
||||||
|
setGameConfigBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshBtn = (
|
const refreshBtn = (
|
||||||
<button onClick={load} className="btn-secondary text-xs" disabled={busy}>
|
<button onClick={load} className="btn-secondary text-xs" disabled={busy}>
|
||||||
<RefreshCw className={busy ? 'animate-spin' : ''} />
|
<RefreshCw className={busy ? 'animate-spin' : ''} />
|
||||||
@@ -308,6 +352,16 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{gameConfigs && (
|
||||||
|
<DiscoveredGameConfigsPanel
|
||||||
|
settings={gameConfigs}
|
||||||
|
busy={gameConfigBusy}
|
||||||
|
onChange={updateGameConfigs}
|
||||||
|
onSave={() => saveGameConfigs(false)}
|
||||||
|
onReset={() => saveGameConfigs(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{weekly && (
|
{weekly && (
|
||||||
<div className="card !p-5 max-w-5xl mt-5">
|
<div className="card !p-5 max-w-5xl mt-5">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||||
@@ -507,3 +561,241 @@ export function Settings({ embedded }: { embedded?: boolean } = {}) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DiscoveredGameConfigsPanel({
|
||||||
|
settings,
|
||||||
|
busy,
|
||||||
|
onChange,
|
||||||
|
onSave,
|
||||||
|
onReset,
|
||||||
|
}: {
|
||||||
|
settings: DiscoveredGameConfigSettings;
|
||||||
|
busy: boolean;
|
||||||
|
onChange: (patch: Partial<DiscoveredGameConfigSettings>) => void;
|
||||||
|
onSave: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}) {
|
||||||
|
const number = (key: keyof DiscoveredGameConfigSettings, value: string) => {
|
||||||
|
const n = Number.parseFloat(value);
|
||||||
|
onChange({ [key]: Number.isFinite(n) ? n : 0 } as Partial<DiscoveredGameConfigSettings>);
|
||||||
|
};
|
||||||
|
|
||||||
|
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">GameConfig</h2>
|
||||||
|
<p className="mt-1 text-xs text-ink-400">
|
||||||
|
Typed keys confirmed in the 2020.12 client decomp.
|
||||||
|
</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">
|
||||||
|
<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">Social</h3>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<NumberConfig
|
||||||
|
label="Friends.PostGamePromptUnderFriendCount"
|
||||||
|
value={settings.friendsPostGamePromptUnderFriendCount}
|
||||||
|
onChange={(v) => number('friendsPostGamePromptUnderFriendCount', v)}
|
||||||
|
/>
|
||||||
|
<NumberConfig
|
||||||
|
label="Friends.SuggestFriendCodeOnFriendsScreenCount"
|
||||||
|
value={settings.friendsSuggestFriendCodeOnFriendsScreenCount}
|
||||||
|
onChange={(v) => number('friendsSuggestFriendCodeOnFriendsScreenCount', 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">Account gates</h3>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<BoolConfig
|
||||||
|
label="Screens.ForceVerification"
|
||||||
|
checked={settings.screensForceVerification}
|
||||||
|
onChange={(v) => onChange({ screensForceVerification: v })}
|
||||||
|
/>
|
||||||
|
<BoolConfig
|
||||||
|
label="VR.ForceVerification"
|
||||||
|
checked={settings.vrForceVerification}
|
||||||
|
onChange={(v) => onChange({ vrForceVerification: 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">Rewards</h3>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<BoolConfig
|
||||||
|
label="Rewards.UseRewardSelection"
|
||||||
|
checked={settings.rewardsUseRewardSelection}
|
||||||
|
onChange={(v) => onChange({ rewardsUseRewardSelection: v })}
|
||||||
|
/>
|
||||||
|
<NumberConfig
|
||||||
|
label="Rewards.SelectionTimeout"
|
||||||
|
value={settings.rewardsSelectionTimeout}
|
||||||
|
onChange={(v) => number('rewardsSelectionTimeout', 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">Room details</h3>
|
||||||
|
<div className="mt-3">
|
||||||
|
<BoolConfig
|
||||||
|
label="RoomDetails.PhotoRollEnabled"
|
||||||
|
checked={settings.roomDetailsPhotoRollEnabled}
|
||||||
|
onChange={(v) => onChange({ roomDetailsPhotoRollEnabled: 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">Networking</h3>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<NumberConfig
|
||||||
|
label="loadingNetworkTimeout"
|
||||||
|
value={settings.loadingNetworkTimeout}
|
||||||
|
onChange={(v) => number('loadingNetworkTimeout', v)}
|
||||||
|
/>
|
||||||
|
<NumberConfig
|
||||||
|
label="runningNetworkTimeout"
|
||||||
|
value={settings.runningNetworkTimeout}
|
||||||
|
onChange={(v) => number('runningNetworkTimeout', 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">Rendering / sync</h3>
|
||||||
|
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||||
|
<BoolConfig
|
||||||
|
label="SynchronizedField.RemoveDefaultEntries"
|
||||||
|
checked={settings.synchronizedFieldRemoveDefaultEntries}
|
||||||
|
onChange={(v) => onChange({ synchronizedFieldRemoveDefaultEntries: v })}
|
||||||
|
/>
|
||||||
|
<BoolConfig
|
||||||
|
label="Rendering.DisableSrpBatcher"
|
||||||
|
checked={settings.renderingDisableSrpBatcher}
|
||||||
|
onChange={(v) => onChange({ renderingDisableSrpBatcher: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div 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">Split tests</h3>
|
||||||
|
<div className="mt-3 grid gap-3 lg:grid-cols-3">
|
||||||
|
<JsonConfig
|
||||||
|
label="splitTestSoftOverrides"
|
||||||
|
value={settings.splitTestSoftOverrides}
|
||||||
|
onChange={(v) => onChange({ splitTestSoftOverrides: v })}
|
||||||
|
/>
|
||||||
|
<JsonConfig
|
||||||
|
label="splitTestHardOverrides"
|
||||||
|
value={settings.splitTestHardOverrides}
|
||||||
|
onChange={(v) => onChange({ splitTestHardOverrides: v })}
|
||||||
|
/>
|
||||||
|
<JsonConfig
|
||||||
|
label="splitTestSegmentProbabilities"
|
||||||
|
value={settings.splitTestSegmentProbabilities}
|
||||||
|
onChange={(v) => onChange({ splitTestSegmentProbabilities: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberConfig({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
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)} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BoolConfig({
|
||||||
|
label,
|
||||||
|
checked,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="flex min-h-10 items-center justify-between gap-3 rounded-md border border-ink-800 bg-ink-900/50 px-3 py-2">
|
||||||
|
<span className="font-mono text-xs text-ink-100">{label}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
className="size-4 rounded border-ink-700 bg-ink-950 text-brand-500 focus:ring-brand-500/30"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function JsonConfig({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<label className="block">
|
||||||
|
<span className="label font-mono normal-case tracking-normal">{label}</span>
|
||||||
|
<textarea
|
||||||
|
className="input mt-1 min-h-28 font-mono text-xs"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultGameConfigs(): DiscoveredGameConfigSettings {
|
||||||
|
return {
|
||||||
|
friendsPostGamePromptUnderFriendCount: 3,
|
||||||
|
friendsSuggestFriendCodeOnFriendsScreenCount: 5,
|
||||||
|
screensForceVerification: false,
|
||||||
|
vrForceVerification: false,
|
||||||
|
rewardsUseRewardSelection: false,
|
||||||
|
rewardsSelectionTimeout: 30,
|
||||||
|
roomDetailsPhotoRollEnabled: false,
|
||||||
|
loadingNetworkTimeout: 30,
|
||||||
|
runningNetworkTimeout: 30,
|
||||||
|
synchronizedFieldRemoveDefaultEntries: false,
|
||||||
|
renderingDisableSrpBatcher: false,
|
||||||
|
splitTestSoftOverrides: '{}',
|
||||||
|
splitTestHardOverrides: '{}',
|
||||||
|
splitTestSegmentProbabilities: '{}',
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user