generated from nhcarrigan/template
feat: add sync new content debug tool
Adds a new debug panel button that injects any adventurers, quests, bosses, equipment, upgrades, achievements, zones, and exploration areas that exist in the current game data but are missing from an existing player save (e.g. content added after the save was first created).
This commit is contained in:
@@ -13,11 +13,15 @@ import {
|
||||
type GameState,
|
||||
} from "@elysium/types";
|
||||
import { Hono } from "hono";
|
||||
import { defaultAchievements } from "../data/achievements.js";
|
||||
import { defaultAdventurers } from "../data/adventurers.js";
|
||||
import { defaultBosses } from "../data/bosses.js";
|
||||
import { defaultEquipment } from "../data/equipment.js";
|
||||
import { defaultExplorations } from "../data/explorations.js";
|
||||
import { initialGameState } from "../data/initialState.js";
|
||||
import { defaultQuests } from "../data/quests.js";
|
||||
import { currentSchemaVersion } from "../data/schemaVersion.js";
|
||||
import { defaultUpgrades } from "../data/upgrades.js";
|
||||
import { defaultZones } from "../data/zones.js";
|
||||
import { prisma } from "../db/client.js";
|
||||
import { authMiddleware } from "../middleware/auth.js";
|
||||
@@ -508,6 +512,85 @@ const applyForceUnlocks = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects any entries from a defaults array that are missing from an existing
|
||||
* saved array (matched by `id`), cloning each new entry before pushing.
|
||||
* @param existing - The player's saved array (mutated in place).
|
||||
* @param defaults - The current default data array to compare against.
|
||||
* @returns The number of entries that were added.
|
||||
*/
|
||||
const injectMissingEntries = <T extends { id: string }>(
|
||||
existing: Array<T>,
|
||||
defaults: Array<T>,
|
||||
): number => {
|
||||
const existingIds = new Set(existing.map((item) => {
|
||||
return item.id;
|
||||
}));
|
||||
let added = 0;
|
||||
for (const item of defaults) {
|
||||
if (!existingIds.has(item.id)) {
|
||||
existing.push(structuredClone(item));
|
||||
added = added + 1;
|
||||
}
|
||||
}
|
||||
return added;
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects any exploration areas from the defaults that are missing from the
|
||||
* player's exploration state, seeding each new area as locked.
|
||||
* @param state - The player's current game state (mutated in place).
|
||||
* @returns The number of exploration areas that were added.
|
||||
*/
|
||||
const injectMissingExplorationAreas = (state: GameState): number => {
|
||||
if (state.exploration === undefined) {
|
||||
return 0;
|
||||
}
|
||||
const existingIds = new Set(state.exploration.areas.map((area) => {
|
||||
return area.id;
|
||||
}));
|
||||
let added = 0;
|
||||
for (const area of defaultExplorations) {
|
||||
if (!existingIds.has(area.id)) {
|
||||
state.exploration.areas.push({ id: area.id, status: "locked" });
|
||||
added = added + 1;
|
||||
}
|
||||
}
|
||||
return added;
|
||||
};
|
||||
|
||||
/* eslint-disable stylistic/max-len -- Long function call lines cannot be shortened without losing alignment */
|
||||
/**
|
||||
* Syncs a player's save with the current game data, injecting any content
|
||||
* entries that are missing because they were added after the save was created.
|
||||
* @param state - The player's current game state (mutated in place).
|
||||
* @returns Counts of how many entries were added per content type.
|
||||
*/
|
||||
const syncNewContent = (
|
||||
state: GameState,
|
||||
): {
|
||||
achievementsAdded: number;
|
||||
adventurersAdded: number;
|
||||
bossesAdded: number;
|
||||
equipmentAdded: number;
|
||||
explorationAreasAdded: number;
|
||||
questsAdded: number;
|
||||
upgradesAdded: number;
|
||||
zonesAdded: number;
|
||||
} => {
|
||||
return {
|
||||
achievementsAdded: injectMissingEntries(state.achievements, defaultAchievements),
|
||||
adventurersAdded: injectMissingEntries(state.adventurers, defaultAdventurers),
|
||||
bossesAdded: injectMissingEntries(state.bosses, defaultBosses),
|
||||
equipmentAdded: injectMissingEntries(state.equipment, defaultEquipment),
|
||||
explorationAreasAdded: injectMissingExplorationAreas(state),
|
||||
questsAdded: injectMissingEntries(state.quests, defaultQuests),
|
||||
upgradesAdded: injectMissingEntries(state.upgrades, defaultUpgrades),
|
||||
zonesAdded: injectMissingEntries(state.zones, defaultZones),
|
||||
};
|
||||
};
|
||||
/* eslint-enable stylistic/max-len -- Re-enable after long lines */
|
||||
|
||||
const debugRouter = new Hono<HonoEnvironment>();
|
||||
debugRouter.use(authMiddleware);
|
||||
|
||||
@@ -572,6 +655,67 @@ debugRouter.post("/force-unlocks", async(context) => {
|
||||
}
|
||||
});
|
||||
|
||||
debugRouter.post("/sync-new-content", async(context) => {
|
||||
try {
|
||||
const discordId = context.get("discordId");
|
||||
|
||||
const gameStateRecord = await prisma.gameState.findUnique({
|
||||
where: { discordId },
|
||||
});
|
||||
if (!gameStateRecord) {
|
||||
return context.json({ error: "No game state found" }, 404);
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma stores state as JSON object */
|
||||
const state = gameStateRecord.state as unknown as GameState;
|
||||
|
||||
const {
|
||||
achievementsAdded,
|
||||
adventurersAdded,
|
||||
bossesAdded,
|
||||
equipmentAdded,
|
||||
explorationAreasAdded,
|
||||
questsAdded,
|
||||
upgradesAdded,
|
||||
zonesAdded,
|
||||
} = syncNewContent(state);
|
||||
|
||||
const updatedAt = Date.now();
|
||||
await prisma.gameState.update({
|
||||
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Prisma requires object */
|
||||
data: { state: state as object, updatedAt: updatedAt },
|
||||
where: { discordId },
|
||||
});
|
||||
|
||||
const secret = process.env.ANTI_CHEAT_SECRET;
|
||||
const signature
|
||||
= secret === undefined
|
||||
? undefined
|
||||
: computeHmac(JSON.stringify(state), secret);
|
||||
|
||||
return context.json({
|
||||
achievementsAdded,
|
||||
adventurersAdded,
|
||||
bossesAdded,
|
||||
equipmentAdded,
|
||||
explorationAreasAdded,
|
||||
questsAdded,
|
||||
signature,
|
||||
state,
|
||||
upgradesAdded,
|
||||
zonesAdded,
|
||||
});
|
||||
} catch (error) {
|
||||
void logger.error(
|
||||
"debug_sync_new_content",
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(String(error)),
|
||||
);
|
||||
return context.json({ error: "Internal server error" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
debugRouter.post("/hard-reset", async(context) => {
|
||||
try {
|
||||
const discordId = context.get("discordId");
|
||||
|
||||
Reference in New Issue
Block a user