Give packs an identity so create can refuse missing deps and load in a stable order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 00:20:36 +03:00
co-authored by Cursor
parent e5a0ae5b9d
commit 263c94c55d
28 changed files with 851 additions and 56 deletions
+4
View File
@@ -40,6 +40,8 @@ const ru = {
errorInvalidStartDate: 'Дата начала вне допустимого диапазона.',
errorInvalidMap: 'Карта должна быть связным графом: двор и хотя бы одна комната.',
errorUnknownMod: 'Выбранный мод не найден.',
errorMissingMod: 'Не выбран обязательный мод «{id}».',
errorModCycle: 'Выбранные моды зависят друг от друга по кругу.',
errorInvalidCatalog: 'Не удалось загрузить выбранные моды.',
errorUnknownNameSet: 'Выбранный набор имён не найден.',
errorUnknownNativeLanguage: 'Выбранный родной язык не входит в этот набор имён.',
@@ -251,6 +253,8 @@ const en: Messages = {
errorInvalidStartDate: 'The start date is outside the allowed range.',
errorInvalidMap: 'The map must be a connected graph: a yard and at least one room.',
errorUnknownMod: 'A selected mod is missing.',
errorMissingMod: 'Required mod “{id}” is not selected.',
errorModCycle: 'The selected mods depend on each other in a cycle.',
errorInvalidCatalog: 'The selected packs could not be loaded.',
errorUnknownNameSet: 'The selected name set is not in the catalog.',
errorUnknownNativeLanguage: 'The selected native language is not in that name set.',
+10 -2
View File
@@ -10,6 +10,7 @@ export interface School {
readonly gameTime: string;
readonly running: boolean;
readonly speedIndex: number;
readonly modIds?: readonly string[];
}
export interface SchoolsResponse {
@@ -30,6 +31,7 @@ export class ApiError extends Error {
readonly payroll?: number,
readonly remaining?: number,
readonly attempted?: number,
readonly missing?: string,
) {
super(message);
}
@@ -75,6 +77,9 @@ export async function createSchool(
export interface ModInfo {
readonly id: string;
readonly required: boolean;
readonly label: string;
readonly version: string;
readonly requires: readonly string[];
}
export interface DefInfo {
@@ -167,8 +172,9 @@ export interface CatalogResponse {
readonly holidays: readonly HolidayInfo[];
}
export async function fetchMods(): Promise<readonly ModInfo[]> {
const response = await request<{ mods: readonly ModInfo[] }>('/api/mods');
export async function fetchMods(lang: string): Promise<readonly ModInfo[]> {
const query = new URLSearchParams({ lang });
const response = await request<{ mods: readonly ModInfo[] }>(`/api/mods?${query.toString()}`);
return response.mods;
}
@@ -528,6 +534,7 @@ async function toApiError(response: Response): Promise<ApiError> {
payroll?: number;
remaining?: number;
attempted?: number;
missing?: string;
};
return new ApiError(
response.status,
@@ -537,6 +544,7 @@ async function toApiError(response: Response): Promise<ApiError> {
problem.payroll,
problem.remaining,
problem.attempted,
problem.missing,
);
} catch {
return new ApiError(response.status, 'unknown', response.statusText);
@@ -194,7 +194,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
const paintMods = async (): Promise<void> => {
let packs;
try {
packs = await fetchMods();
packs = await fetchMods(getLocale());
} catch {
showError(t('catalogLoadFailed'));
return;
@@ -223,7 +223,7 @@ export function createSchoolDialog(options: CreateSchoolOptions): Promise<School
'label',
{ class: 'mod-list__item' },
checkbox,
pack.required ? t('coreModLocked', { id: pack.id }) : pack.id,
pack.required ? t('coreModLocked', { id: pack.label }) : pack.label,
);
modsList.append(label);
}
@@ -359,6 +359,10 @@ function describe(reason: unknown): string {
return t('errorInvalidMap');
case 'unknown-mod':
return t('errorUnknownMod');
case 'missing-mod':
return t('errorMissingMod', { id: reason.missing ?? '' });
case 'mod-cycle':
return t('errorModCycle');
case 'invalid-catalog':
return t('errorInvalidCatalog');
case 'unknown-name-set':