Files
Wand-Enhancer/web-panel/bridge/bridge-modules/normalizers.cjs
T

485 lines
15 KiB
JavaScript

const { KNOWN_CHEAT_TYPES } = require('./constants.cjs');
const { cloneValue, firstString, isRecord, safeString, toStringId } = require('./utils.cjs');
function normalizeOption(option) {
if (typeof option === 'string' || typeof option === 'number') {
return {
label: String(option),
value: option,
};
}
if (!isRecord(option)) {
return null;
}
const value = option.value;
if (typeof value !== 'string' && typeof value !== 'number') {
return null;
}
return {
label: safeString(option.label, String(value)),
value,
};
}
function normalizeArgs(args) {
if (!isRecord(args)) {
return {};
}
const next = {};
if (typeof args.min === 'number') next.min = args.min;
if (typeof args.max === 'number') next.max = args.max;
if (typeof args.step === 'number') next.step = args.step;
if (typeof args.postfix === 'string') next.postfix = args.postfix;
if (typeof args.default === 'string' || typeof args.default === 'number' || typeof args.default === 'boolean') {
next.default = args.default;
}
if (Array.isArray(args.options)) {
next.options = args.options.map(normalizeOption).filter(Boolean);
}
if (typeof args.button === 'string' || typeof args.button === 'boolean') {
next.button = args.button;
}
return next;
}
function normalizeCheat(cheat, index) {
if (!isRecord(cheat)) {
return null;
}
const target = safeString(cheat.target);
const type = safeString(cheat.type);
if (!target || !KNOWN_CHEAT_TYPES.has(type)) {
return null;
}
const normalized = {
uuid: safeString(cheat.uuid, `${target}-${index}`),
target,
type,
name: safeString(cheat.name, target),
description: typeof cheat.description === 'string' ? cheat.description : null,
instructions: typeof cheat.instructions === 'string' ? cheat.instructions : null,
category: safeString(cheat.category, 'general'),
parent: typeof cheat.parent === 'string' ? cheat.parent : null,
args: normalizeArgs(cheat.args),
};
if (typeof cheat.flags === 'number') {
normalized.flags = cheat.flags;
}
if (Array.isArray(cheat.hotkeys)) {
normalized.hotkeys = cheat.hotkeys.filter(Array.isArray).map((group) => group.map((item) => String(item)));
}
return normalized;
}
function normalizeImageUrl(...values) {
const value = firstString(...values);
return value || null;
}
function getRawInstalledApps(rawSnapshot) {
if (Array.isArray(rawSnapshot)) {
return rawSnapshot;
}
if (isRecord(rawSnapshot) && Array.isArray(rawSnapshot.apps)) {
return rawSnapshot.apps;
}
if (isRecord(rawSnapshot) && Array.isArray(rawSnapshot.installedApps)) {
return rawSnapshot.installedApps;
}
return null;
}
function normalizeInstalledApp(app) {
if (!isRecord(app)) {
return null;
}
const platform = safeString(app.platform);
const sku = safeString(app.sku);
if (!platform || !sku) {
return null;
}
const location = typeof app.location === 'string' ? app.location : '';
const alternateLocations = Array.isArray(app.alternateLocations)
? app.alternateLocations.filter((entry) => typeof entry === 'string' && entry.trim()).map((entry) => entry.trim())
: [];
return {
platform,
sku,
correlationId: `${platform}:${sku}`,
displayName: firstString(
app.displayName,
app.titleName,
app.gameName,
app.name,
location.replaceAll('\\', '/').split('/').filter(Boolean).pop() || '',
`${platform}:${sku}`
),
gameId: toStringId(app.gameId),
titleId: toStringId(app.titleId),
location,
alternateLocations,
imageUrl: normalizeImageUrl(app.imageUrl, app.iconUrl, app.coverUrl, app.thumbnailUrl, app.logoUrl, app.headerImageUrl),
platformLastPlayedTimestamp: typeof app.platformLastPlayedTimestamp === 'number' ? app.platformLastPlayedTimestamp : null,
platformTotalPlaytimeMinutes: typeof app.platformTotalPlaytimeMinutes === 'number' ? app.platformTotalPlaytimeMinutes : null,
};
}
function normalizeInstalledAppsSnapshot(rawSnapshot) {
const rawApps = getRawInstalledApps(rawSnapshot);
if (!rawApps) {
return null;
}
const apps = rawApps.map(normalizeInstalledApp).filter(Boolean).sort(compareInstalledApps);
const diagnostics = isRecord(rawSnapshot) && isRecord(rawSnapshot.diagnostics)
? cloneValue(rawSnapshot.diagnostics)
: null;
return {
instanceId: isRecord(rawSnapshot) ? safeString(rawSnapshot.instanceId, 'wand-installed-apps') : 'wand-installed-apps',
updatedAt: isRecord(rawSnapshot) && typeof rawSnapshot.updatedAt === 'string' ? rawSnapshot.updatedAt : new Date().toISOString(),
apps,
diagnostics,
};
}
function normalizeGameStatusSnapshot(rawSnapshot) {
if (!isRecord(rawSnapshot)) {
return null;
}
const rawSession = isRecord(rawSnapshot.session) ? rawSnapshot.session : {};
const rawTrainer = isRecord(rawSnapshot.trainer) ? rawSnapshot.trainer : {};
return {
instanceId: safeString(rawSnapshot.instanceId, 'wand-game-status'),
updatedAt: typeof rawSnapshot.updatedAt === 'string' ? rawSnapshot.updatedAt : new Date().toISOString(),
session: {
state: rawSession.state === 'running' ? 'running' : 'idle',
event: safeString(rawSession.event, 'snapshot'),
processId: typeof rawSession.processId === 'number' ? rawSession.processId : null,
gameId: toStringId(rawSession.gameId),
titleId: toStringId(rawSession.titleId),
titleName: typeof rawSession.titleName === 'string' ? rawSession.titleName : null,
sessionDurationSeconds: typeof rawSession.sessionDurationSeconds === 'number' ? rawSession.sessionDurationSeconds : null,
startedAt: typeof rawSession.startedAt === 'string' ? rawSession.startedAt : null,
endedAt: typeof rawSession.endedAt === 'string' ? rawSession.endedAt : null,
},
trainer: {
state: rawTrainer.state === 'running' ? 'running' : 'idle',
event: safeString(rawTrainer.event, 'snapshot'),
trainerId: toStringId(rawTrainer.trainerId),
displayName: typeof rawTrainer.displayName === 'string' ? rawTrainer.displayName : null,
gameId: toStringId(rawTrainer.gameId),
titleId: toStringId(rawTrainer.titleId),
},
};
}
function normalizeRemoteCommandAction(value) {
if (value === 'launch' || value === 'stop') {
return value;
}
return null;
}
function normalizeRemoteCommandResult(rawResult, fallback) {
const action = normalizeRemoteCommandAction(isRecord(rawResult) ? rawResult.action : null) || fallback.action;
const gameId = isRecord(rawResult) ? toStringId(rawResult.gameId) || fallback.gameId || null : fallback.gameId || null;
const titleId = isRecord(rawResult) ? toStringId(rawResult.titleId) || fallback.titleId || null : fallback.titleId || null;
const ok = rawResult === true || Boolean(isRecord(rawResult) && rawResult.ok === true);
const payload = {
ok,
action,
gameId,
titleId,
};
if (ok) {
return payload;
}
if (!isRecord(rawResult) || !isRecord(rawResult.error)) {
return {
...payload,
error: {
code: 'command_rejected',
message: 'The renderer rejected the remote command.',
},
};
}
return {
...payload,
error: {
code: safeString(rawResult.error.code, 'command_rejected'),
message: safeString(rawResult.error.message, 'The renderer rejected the remote command.'),
},
};
}
function summarizeInstalledAppsSource(rawSnapshot) {
if (!isRecord(rawSnapshot) || !isRecord(rawSnapshot.diagnostics)) {
return '';
}
const parts = [];
for (const key of ['rawInstalledApps', 'catalogGames', 'catalogTitles']) {
const value = rawSnapshot.diagnostics[key];
if (typeof value === 'number') {
parts.push(`${key}=${value}`);
}
}
return parts.join(', ');
}
function installedAppsSignature(snapshot) {
return snapshot.apps
.map((app) => [
app.platform,
app.sku,
app.displayName,
app.gameId || '',
app.titleId || '',
app.location,
app.imageUrl || '',
app.platformLastPlayedTimestamp || '',
app.platformTotalPlaytimeMinutes || '',
].join('|'))
.join('\n');
}
function gameStatusSignature(snapshot) {
return [
snapshot.session.state,
snapshot.session.event,
snapshot.session.processId || '',
snapshot.session.gameId || '',
snapshot.session.titleId || '',
snapshot.session.titleName || '',
snapshot.session.sessionDurationSeconds || '',
snapshot.session.startedAt || '',
snapshot.session.endedAt || '',
snapshot.trainer.state,
snapshot.trainer.event,
snapshot.trainer.trainerId || '',
snapshot.trainer.displayName || '',
snapshot.trainer.gameId || '',
snapshot.trainer.titleId || '',
].join('|');
}
function buildInstalledAppsDebugPayload(snapshot) {
if (!snapshot) {
return {
ok: false,
instanceId: null,
updatedAt: null,
counts: {
myGamesEntries: 0,
rawInstallEntries: 0,
groupedTitles: 0,
uniqueTitleIds: 0,
uniqueGameIds: 0,
},
diagnostics: null,
byPlatform: {},
titles: [],
apps: [],
};
}
const diagnostics = isRecord(snapshot.diagnostics) ? snapshot.diagnostics : null;
const byPlatform = {};
const uniqueTitleIds = new Set();
const uniqueGameIds = new Set();
const titleGroups = new Map();
for (const app of snapshot.apps) {
byPlatform[app.platform] = (byPlatform[app.platform] || 0) + 1;
if (app.titleId) {
uniqueTitleIds.add(app.titleId);
}
if (app.gameId) {
uniqueGameIds.add(app.gameId);
}
const groupKey = resolveInstalledAppGroupKey(app);
let group = titleGroups.get(groupKey);
if (!group) {
group = {
key: groupKey,
titleId: app.titleId,
displayName: app.displayName,
gameIds: new Set(),
platforms: new Set(),
apps: [],
};
titleGroups.set(groupKey, group);
}
if (app.gameId) {
group.gameIds.add(app.gameId);
}
group.platforms.add(app.platform);
group.apps.push(app);
}
const titles = Array.from(titleGroups.values())
.map((group) => ({
key: group.key,
titleId: group.titleId,
displayName: group.displayName,
gameIds: Array.from(group.gameIds).sort(),
platforms: Array.from(group.platforms).sort(),
appEntries: group.apps.length,
apps: group.apps,
}))
.sort((left, right) => left.displayName.localeCompare(right.displayName));
return {
ok: true,
instanceId: snapshot.instanceId,
updatedAt: snapshot.updatedAt,
counts: {
myGamesEntries: snapshot.apps.length,
rawInstallEntries: typeof diagnostics?.rawInstalledApps === 'number' ? diagnostics.rawInstalledApps : snapshot.apps.length,
groupedTitles: titles.length,
uniqueTitleIds: uniqueTitleIds.size,
uniqueGameIds: uniqueGameIds.size,
},
diagnostics,
byPlatform,
titles,
apps: snapshot.apps,
};
}
function normalizeSnapshot(rawSnapshot) {
if (!isRecord(rawSnapshot) || !isRecord(rawSnapshot.metadata) || !isRecord(rawSnapshot.metadata.info)) {
return null;
}
const info = rawSnapshot.metadata.info;
const blueprint = isRecord(info.blueprint) ? info.blueprint : {};
const rawCheats = Array.isArray(blueprint.cheats) ? blueprint.cheats : [];
const cheats = rawCheats.map(normalizeCheat).filter(Boolean);
const categories = Array.from(new Set(cheats.map((entry) => entry.category)));
const trainerId = safeString(rawSnapshot.trainerId || rawSnapshot.trainerInfo?.trainerId);
const displayName = firstString(
rawSnapshot.trainerInfo?.displayName,
rawSnapshot.trainerInfo?.gameName,
rawSnapshot.trainerInfo?.titleName,
rawSnapshot.trainerInfo?.title,
rawSnapshot.trainerInfo?.name,
info.displayName,
info.gameName,
info.titleName,
info.title,
info.name,
info.game?.displayName,
info.game?.name,
info.game?.title
);
if (!trainerId) {
return null;
}
const trainerMeta = {
session: {
instanceId: safeString(rawSnapshot.instanceId, 'wand-session'),
accessToken: safeString(rawSnapshot.accessToken),
},
trainer: {
trainerId,
gameId: safeString(rawSnapshot.trainerInfo?.gameId || info.gameId),
displayName: displayName || safeString(rawSnapshot.trainerInfo?.gameId || info.gameId, trainerId),
titleId: typeof info.titleId === 'string' ? info.titleId : null,
gameVersion: typeof rawSnapshot.gameVersion === 'string' ? rawSnapshot.gameVersion : null,
trainerLoading: rawSnapshot.trainerLoading === true,
gameInstalled: rawSnapshot.gameInstalled !== false,
needsCompatibilityWarning: rawSnapshot.needsCompatibilityWarning === true,
language: safeString(rawSnapshot.language, 'en-US'),
themeId: safeString(rawSnapshot.themeId, 'default'),
isTimeLimitExpired: rawSnapshot.isTimeLimitExpired === true,
notesReadHash: typeof rawSnapshot.notesReadHash === 'string' ? rawSnapshot.notesReadHash : null,
},
schema: {
categories,
cheats,
},
};
const trainerValues = {
trainerId,
values: isRecord(rawSnapshot.values) ? cloneValue(rawSnapshot.values) : {},
};
return {
trainerMeta,
trainerValues,
};
}
function compareInstalledApps(left, right) {
const displayNameDiff = left.displayName.localeCompare(right.displayName);
if (displayNameDiff !== 0) {
return displayNameDiff;
}
const platformDiff = left.platform.localeCompare(right.platform);
if (platformDiff !== 0) {
return platformDiff;
}
return left.sku.localeCompare(right.sku);
}
function resolveInstalledAppGroupKey(app) {
if (app.titleId) {
return `title:${app.titleId}`;
}
if (app.gameId) {
return `game:${app.gameId}`;
}
return `app:${app.correlationId}`;
}
module.exports = {
buildInstalledAppsDebugPayload,
gameStatusSignature,
installedAppsSignature,
normalizeGameStatusSnapshot,
normalizeInstalledAppsSnapshot,
normalizeRemoteCommandAction,
normalizeRemoteCommandResult,
normalizeSnapshot,
summarizeInstalledAppsSource,
};