139 lines
4 KiB
TypeScript
139 lines
4 KiB
TypeScript
import { builtInPet } from "./built-in-familiar.js";
|
|
import type { Point } from "./display.js";
|
|
import {
|
|
createDefaultAnalyticsState,
|
|
normalizeAnalytics,
|
|
type FamiliarOSActivityRecord,
|
|
type FamiliarOSAnalyticsState,
|
|
} from "./app-state-analytics.js";
|
|
import {
|
|
normalizeInstalledPetRecord,
|
|
validateInstalledPetFiles,
|
|
type FamiliarStateRecord,
|
|
} from "./app-state-familiar-records.js";
|
|
import {
|
|
createDefaultPreferences,
|
|
normalizePreferences,
|
|
type FamiliarOSPreferences,
|
|
} from "./app-state-preferences.js";
|
|
import { assertSafePetId, getInstalledPetDir } from "./familiar-paths.js";
|
|
|
|
export interface InstalledPetState {
|
|
readonly id: string;
|
|
readonly displayName: string;
|
|
readonly description?: string;
|
|
readonly builtIn: boolean;
|
|
readonly protected: boolean;
|
|
readonly installed: boolean;
|
|
readonly source?: {
|
|
readonly kind?: "catalog";
|
|
readonly catalogVersion: 2;
|
|
readonly zip: string;
|
|
readonly preview: string;
|
|
} | {
|
|
readonly kind: "codex";
|
|
readonly path: string;
|
|
};
|
|
readonly broken?: boolean;
|
|
readonly brokenReason?: string;
|
|
}
|
|
|
|
export interface FamiliarOSStateV1 {
|
|
readonly version: 1;
|
|
readonly preferences: FamiliarOSPreferences;
|
|
readonly familiars: {
|
|
readonly installed: readonly InstalledPetState[];
|
|
};
|
|
readonly defaultPet: {
|
|
readonly position?: Point;
|
|
};
|
|
readonly analytics: FamiliarOSAnalyticsState;
|
|
}
|
|
|
|
export function normalizeState(value: unknown): FamiliarOSStateV1 {
|
|
const record = isRecord(value) ? value : {};
|
|
const defaultPetRecord = isRecord(record.defaultPet) ? record.defaultPet : {};
|
|
const preferencesRecord = isRecord(record.preferences) ? record.preferences : {};
|
|
const defaultState = createDefaultState();
|
|
const position = normalizeMaybePosition(defaultPetRecord.position);
|
|
const installedPets = normalizeInstalledPets(record);
|
|
const defaultPetId = typeof preferencesRecord.defaultPetId === "string"
|
|
&& installedPets.some((familiar) => familiar.id === preferencesRecord.defaultPetId && !familiar.broken)
|
|
? preferencesRecord.defaultPetId
|
|
: builtInPet.id;
|
|
|
|
return {
|
|
version: 1,
|
|
preferences: normalizePreferences({
|
|
...defaultState.preferences,
|
|
...preferencesRecord,
|
|
defaultPetId,
|
|
}),
|
|
familiars: {
|
|
installed: installedPets,
|
|
},
|
|
defaultPet: position ? { position } : {},
|
|
analytics: normalizeAnalytics(record.analytics),
|
|
};
|
|
}
|
|
|
|
export function createDefaultState(): FamiliarOSStateV1 {
|
|
return {
|
|
version: 1,
|
|
preferences: createDefaultPreferences(),
|
|
familiars: {
|
|
installed: [builtInPet],
|
|
},
|
|
defaultPet: {},
|
|
analytics: createDefaultAnalyticsState(),
|
|
};
|
|
}
|
|
|
|
export function cloneState(state: FamiliarOSStateV1): FamiliarOSStateV1 {
|
|
return structuredClone(state) as FamiliarOSStateV1;
|
|
}
|
|
|
|
export function normalizePosition(value: Partial<Point>): Point | undefined {
|
|
if (typeof value.x !== "number" || typeof value.y !== "number") {
|
|
return undefined;
|
|
}
|
|
|
|
if (!Number.isFinite(value.x) || !Number.isFinite(value.y)) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
x: Math.round(value.x),
|
|
y: Math.round(value.y),
|
|
};
|
|
}
|
|
|
|
function normalizeInstalledPets(value: Record<string, unknown>): InstalledPetState[] {
|
|
const installed = isRecord(value.familiars) && Array.isArray(value.familiars.installed)
|
|
? value.familiars.installed
|
|
: [];
|
|
|
|
const normalized = installed
|
|
.map((familiar) => normalizeInstalledPetRecord(familiar, {
|
|
builtInPetId: builtInPet.id,
|
|
assertSafePetId,
|
|
validateInstalledPetFiles: (petId) => validateInstalledPetFiles(petId, getInstalledPetDir),
|
|
}))
|
|
.filter((familiar): familiar is FamiliarStateRecord => Boolean(familiar && familiar.id !== builtInPet.id));
|
|
|
|
return [builtInPet, ...normalized];
|
|
}
|
|
|
|
function normalizeMaybePosition(value: unknown): Point | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
|
|
return normalizePosition(value);
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
export type { FamiliarOSActivityRecord };
|