diff --git a/resources/images/test/test.png b/resources/images/test/test.png new file mode 100644 index 000000000..c915f2cc5 Binary files /dev/null and b/resources/images/test/test.png differ diff --git a/src/client/CosmeticPackLoader.ts b/src/client/CosmeticPackLoader.ts new file mode 100644 index 000000000..b1afef9c0 --- /dev/null +++ b/src/client/CosmeticPackLoader.ts @@ -0,0 +1,22 @@ +export function fetchUrl( + packId: string | undefined, + type: string, +): string | undefined { + // TODO: Fetches the resource URL from the API server. + + // Request parameters: + // - packKey: identifier of the cosmetic pack + // - type: asset type (e.g., "structurePort", "structureCity") + // Response: + // - URL string pointing to the requested asset + + // Even if this approach changes, this function will be responsible for obtaining the URL by some method. + + switch (packId) { + case "base": + return; + case "test": + return "/images/test/test.png"; // Example URL for testing + } + return; +} diff --git a/src/client/Main.ts b/src/client/Main.ts index 37ae29027..aa0c3b6f9 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -564,6 +564,38 @@ class Client { this.flagInput === null || this.flagInput.getCurrentFlag() === "xx" ? "" : this.flagInput.getCurrentFlag(), + structurePort: + this.userSettings.getSelectedStructurePort() ?? undefined, + structureCity: + this.userSettings.getSelectedStructureCity() ?? undefined, + structureFactory: + this.userSettings.getSelectedStructureFactory() ?? undefined, + structureMissilesilo: + this.userSettings.getSelectedStructureMissilesilo() ?? undefined, + structureDefensepost: + this.userSettings.getSelectedStructureDefensepost() ?? undefined, + structureSamlauncher: + this.userSettings.getSelectedStructureSamlauncher() ?? undefined, + + spriteTransportship: + this.userSettings.getSelectedSpriteTransportship() ?? undefined, + spriteWarship: + this.userSettings.getSelectedSpriteWarship() ?? undefined, + spriteSammissile: + this.userSettings.getSelectedSpriteSammissile() ?? undefined, + spriteAtombomb: + this.userSettings.getSelectedSpriteAtombomb() ?? undefined, + spriteHydrogenbomb: + this.userSettings.getSelectedSpriteHydrogenbomb() ?? undefined, + spriteTradeship: + this.userSettings.getSelectedSpriteTradeship() ?? undefined, + spriteMirv: this.userSettings.getSelectedSpriteMirv() ?? undefined, + spriteEngine: + this.userSettings.getSelectedSpriteEngine() ?? undefined, + spriteCarriage: + this.userSettings.getSelectedSpriteCarriage() ?? undefined, + spriteLoadedcarriage: + this.userSettings.getSelectedSpriteLoadedcarriage() ?? undefined, }, playerName: this.usernameInput?.getCurrentUsername() ?? "", token: getPlayToken(), diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 01e62f928..95f048511 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -21,6 +21,7 @@ import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import "./components/Difficulties"; import "./components/Maps"; +import { fetchUrl } from "./CosmeticPackLoader"; import { fetchCosmetics } from "./Cosmetics"; import { FlagInput } from "./FlagInput"; import { JoinLobbyEvent } from "./Main"; @@ -469,6 +470,84 @@ export class SinglePlayerModal extends LitElement { : flagInput.getCurrentFlag(), pattern: selectedPattern ?? undefined, color: selectedColor ? { color: selectedColor } : undefined, + pack: { + structurePort: fetchUrl( + this.userSettings.getSelectedStructurePort() ?? undefined, + "structurePort", + ), + structureCity: fetchUrl( + this.userSettings.getSelectedStructureCity() ?? undefined, + "structureCity", + ), + structureFactory: fetchUrl( + this.userSettings.getSelectedStructureFactory() ?? + undefined, + "structureFactory", + ), + structureMissilesilo: fetchUrl( + this.userSettings.getSelectedStructureMissilesilo() ?? + undefined, + "structureMissilesilo", + ), + structureDefensepost: fetchUrl( + this.userSettings.getSelectedStructureDefensepost() ?? + undefined, + "structureDefensepost", + ), + structureSamlauncher: fetchUrl( + this.userSettings.getSelectedStructureSamlauncher() ?? + undefined, + "structureSamlauncher", + ), + + spriteTransportship: fetchUrl( + this.userSettings.getSelectedSpriteTransportship() ?? + undefined, + "spriteTransportship", + ), + spriteWarship: fetchUrl( + this.userSettings.getSelectedSpriteWarship() ?? undefined, + "spriteWarship", + ), + spriteSammissile: fetchUrl( + this.userSettings.getSelectedSpriteSammissile() ?? + undefined, + "spriteSammissile", + ), + spriteAtombomb: fetchUrl( + this.userSettings.getSelectedSpriteAtombomb() ?? + undefined, + "spriteAtombomb", + ), + spriteHydrogenbomb: fetchUrl( + this.userSettings.getSelectedSpriteHydrogenbomb() ?? + undefined, + "spriteHydrogenbomb", + ), + spriteTradeship: fetchUrl( + this.userSettings.getSelectedSpriteTradeship() ?? + undefined, + "spriteTradeship", + ), + spriteMirv: fetchUrl( + this.userSettings.getSelectedSpriteMirv() ?? undefined, + "spriteMirv", + ), + spriteEngine: fetchUrl( + this.userSettings.getSelectedSpriteEngine() ?? undefined, + "spriteEngine", + ), + spriteCarriage: fetchUrl( + this.userSettings.getSelectedSpriteCarriage() ?? + undefined, + "spriteCarriage", + ), + spriteLoadedcarriage: fetchUrl( + this.userSettings.getSelectedSpriteLoadedcarriage() ?? + undefined, + "spriteLoadedcarriage", + ), + }, }, }, ], diff --git a/src/client/graphics/SpriteLoader.ts b/src/client/graphics/SpriteLoader.ts index 29d5b7791..aa3b438e9 100644 --- a/src/client/graphics/SpriteLoader.ts +++ b/src/client/graphics/SpriteLoader.ts @@ -12,6 +12,7 @@ import warshipSprite from "../../../resources/sprites/warship.png"; import { Theme } from "../../core/configuration/Config"; import { TrainType, UnitType } from "../../core/game/Game"; import { UnitView } from "../../core/game/GameView"; +import { PlayerPack } from "../../core/Schemas"; // Can't reuse TrainType because "loaded" is not a type, just an attribute const TrainTypeSprite = { @@ -22,36 +23,55 @@ const TrainTypeSprite = { type TrainTypeSprite = (typeof TrainTypeSprite)[keyof typeof TrainTypeSprite]; -const SPRITE_CONFIG: Partial> = { - [UnitType.TransportShip]: transportShipSprite, - [UnitType.Warship]: warshipSprite, - [UnitType.SAMMissile]: samMissileSprite, - [UnitType.AtomBomb]: atomBombSprite, - [UnitType.HydrogenBomb]: hydrogenBombSprite, - [UnitType.TradeShip]: tradeShipSprite, - [UnitType.MIRV]: mirvSprite, - [TrainTypeSprite.Engine]: trainEngineSprite, - [TrainTypeSprite.Carriage]: trainCarriageSprite, - [TrainTypeSprite.LoadedCarriage]: trainLoadedCarriageSprite, +const SPRITE_CONFIG: Partial< + Record +> = { + [UnitType.TransportShip]: { + key: "spriteTransportship", + url: transportShipSprite, + }, + [UnitType.Warship]: { key: "spriteWarship", url: warshipSprite }, + [UnitType.SAMMissile]: { key: "spriteSammissile", url: samMissileSprite }, + [UnitType.AtomBomb]: { key: "spriteAtombomb", url: atomBombSprite }, + [UnitType.HydrogenBomb]: { + key: "spriteHydrogenbomb", + url: hydrogenBombSprite, + }, + [UnitType.TradeShip]: { key: "spriteTradeship", url: tradeShipSprite }, + [UnitType.MIRV]: { key: "spriteMirv", url: mirvSprite }, + [TrainTypeSprite.Engine]: { key: "spriteEngine", url: trainEngineSprite }, + [TrainTypeSprite.Carriage]: { + key: "spriteCarriage", + url: trainCarriageSprite, + }, + [TrainTypeSprite.LoadedCarriage]: { + key: "spriteLoadedcarriage", + url: trainLoadedCarriageSprite, + }, }; const spriteMap: Map = new Map(); // preload all images -export const loadAllSprites = async (): Promise => { +export const loadAllSprites = async (pack: PlayerPack): Promise => { const entries = Object.entries(SPRITE_CONFIG); const totalSprites = entries.length; let loadedCount = 0; await Promise.all( - entries.map(async ([unitType, url]) => { + entries.map(async ([unitType, value]) => { const typedUnitType = unitType as UnitType | TrainTypeSprite; - if (!url || url === "") { - console.warn(`No sprite URL for ${typedUnitType}, skipping...`); + const key = value?.key; + const fallbackUrl = value?.url; + + if (!fallbackUrl) { + console.warn(`No sprite url for ${typedUnitType}, skipping...`); return; } + const url = pack?.[key] ?? fallbackUrl; + try { const img = new Image(); img.crossOrigin = "anonymous"; diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index bec07483e..11357e68f 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -1,19 +1,19 @@ import { colord, Colord } from "colord"; -import { Theme } from "../../../core/configuration/Config"; -import { EventBus } from "../../../core/EventBus"; -import { TransformHandler } from "../TransformHandler"; -import { Layer } from "./Layer"; - import cityIcon from "../../../../resources/images/buildings/cityAlt1.png"; import factoryIcon from "../../../../resources/images/buildings/factoryAlt1.png"; import shieldIcon from "../../../../resources/images/buildings/fortAlt3.png"; import anchorIcon from "../../../../resources/images/buildings/port1.png"; import missileSiloIcon from "../../../../resources/images/buildings/silo1.png"; import SAMMissileIcon from "../../../../resources/images/buildings/silo4.png"; +import { Theme } from "../../../core/configuration/Config"; +import { EventBus } from "../../../core/EventBus"; import { Cell, UnitType } from "../../../core/game/Game"; import { euclDistFN, isometricDistFN } from "../../../core/game/GameMap"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView, UnitView } from "../../../core/game/GameView"; +import { PlayerPack } from "../../../core/Schemas"; +import { TransformHandler } from "../TransformHandler"; +import { Layer } from "./Layer"; const underConstructionColor = colord({ r: 150, g: 150, b: 150 }); @@ -25,6 +25,7 @@ const ZOOM_THRESHOLD = 4.3; // below this zoom level, structures are not rendere interface UnitRenderConfig { icon: string; + key: string; borderRadius: number; territoryRadius: number; } @@ -34,38 +35,46 @@ export class StructureLayer implements Layer { private context: CanvasRenderingContext2D; private unitIcons: Map = new Map(); private theme: Theme; + private pack: PlayerPack; + private structureLoaded = false; private tempCanvas: HTMLCanvasElement; private tempContext: CanvasRenderingContext2D; // Configuration for supported unit types only - private readonly unitConfigs: Partial> = { + private unitConfigs: Partial> = { [UnitType.Port]: { icon: anchorIcon, + key: "structurePort", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.City]: { icon: cityIcon, + key: "structureCity", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.Factory]: { icon: factoryIcon, + key: "structureFactory", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.MissileSilo]: { icon: missileSiloIcon, + key: "structureMissilesilo", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.DefensePost]: { icon: shieldIcon, + key: "structureDefensepost", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, [UnitType.SAMLauncher]: { icon: SAMMissileIcon, + key: "structureSamlauncher", borderRadius: BASE_BORDER_RADIUS * RADIUS_SCALE_FACTOR, territoryRadius: BASE_TERRITORY_RADIUS * RADIUS_SCALE_FACTOR, }, @@ -86,6 +95,7 @@ export class StructureLayer implements Layer { private loadIcon(unitType: string, config: UnitRenderConfig) { const image = new Image(); + console.log(`loading icon for ${unitType} from ${config.icon}`); image.src = config.icon; image.onload = () => { this.unitIcons.set(unitType, image); @@ -98,8 +108,9 @@ export class StructureLayer implements Layer { }; } - private loadIconData() { + private async loadIconData() { Object.entries(this.unitConfigs).forEach(([unitType, config]) => { + config.icon = this.pack?.[config.key] ?? config.icon; this.loadIcon(unitType, config); }); } @@ -116,6 +127,14 @@ export class StructureLayer implements Layer { if (unit === undefined) continue; this.handleUnitRendering(unit); } + if (!this.structureLoaded) { + const myPlayer = this.game.myPlayer(); + if (myPlayer) { + this.pack = myPlayer.cosmetics.pack ?? {}; + this.loadIconData(); + this.structureLoaded = true; + } + } } init() { diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 4945969be..90c7faacf 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -14,6 +14,7 @@ import { MoveWarshipIntentEvent } from "../../Transport"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; +import { PlayerPack } from "../../../core/Schemas"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { getColoredSprite, @@ -36,6 +37,8 @@ export class UnitLayer implements Layer { private unitToTrail = new Map(); private theme: Theme; + private pack: PlayerPack; + private spritesLoaded = false; private alternateView = false; @@ -68,6 +71,15 @@ export class UnitLayer implements Layer { ?.[GameUpdateType.Unit]?.map((unit) => unit.id); this.updateUnitsSprites(unitIds ?? []); + + if (!this.spritesLoaded) { + const myPlayer = this.game.myPlayer(); + if (myPlayer) { + this.pack = myPlayer.cosmetics.pack ?? {}; + loadAllSprites(this.pack); + this.spritesLoaded = true; + } + } } init() { @@ -75,8 +87,7 @@ export class UnitLayer implements Layer { this.eventBus.on(MouseUpEvent, (e) => this.onMouseUp(e)); this.eventBus.on(UnitSelectionEvent, (e) => this.onUnitSelectionChange(e)); this.redraw(); - - loadAllSprites(); + loadAllSprites(this.pack); } /** diff --git a/src/core/CosmeticSchemas.ts b/src/core/CosmeticSchemas.ts index a4bcd6762..0f575848b 100644 --- a/src/core/CosmeticSchemas.ts +++ b/src/core/CosmeticSchemas.ts @@ -94,3 +94,32 @@ export const DefaultPattern = { patternData: "AAAAAA", colorPalette: undefined, } satisfies PlayerPattern; + +const imageFile = z + .string() + .regex(/\.(png|webp|jpg|jpeg|gif)$/i, "Invalid image extension"); +const audioFile = z + .string() + .regex(/\.(ogg|mp3|wav|m4a)$/i, "Invalid audio extension"); + +const ImageAssetNode: z.ZodType = z.lazy(() => + z.union([imageFile, z.record(z.string(), ImageAssetNode)]), +); +const AudioAssetNode: z.ZodType = z.lazy(() => + z.union([audioFile, z.record(z.string(), AudioAssetNode)]), +); + +export const CosmeticManifestSchema = z + .object({ + id: z.string(), + name: z.string(), + assets: z + .object({ + structure: z.record(z.string(), ImageAssetNode).optional(), + sprites: z.record(z.string(), ImageAssetNode).optional(), + audio: z.record(z.string(), AudioAssetNode).optional(), + }) + .strict(), + }) + .strict(); +export type CosmeticManifest = z.infer; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 270cc2297..bdaab81f6 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -390,6 +390,24 @@ export const PlayerCosmeticRefsSchema = z.object({ color: z.string().optional(), patternName: PatternNameSchema.optional(), patternColorPaletteName: z.string().optional(), + + structurePort: z.string().optional(), + structureCity: z.string().optional(), + structureFactory: z.string().optional(), + structureMissilesilo: z.string().optional(), + structureDefensepost: z.string().optional(), + structureSamlauncher: z.string().optional(), + + spriteTransportship: z.string().optional(), + spriteWarship: z.string().optional(), + spriteSammissile: z.string().optional(), + spriteAtombomb: z.string().optional(), + spriteHydrogenbomb: z.string().optional(), + spriteTradeship: z.string().optional(), + spriteMirv: z.string().optional(), + spriteEngine: z.string().optional(), + spriteCarriage: z.string().optional(), + spriteLoadedcarriage: z.string().optional(), }); export const PlayerPatternSchema = z.object({ @@ -402,10 +420,33 @@ export const PlayerColorSchema = z.object({ color: z.string(), }); + +export const PlayerPackSchema = z.object({ + structurePort: z.string().optional(), + structureCity: z.string().optional(), + structureFactory: z.string().optional(), + structureMissilesilo: z.string().optional(), + structureDefensepost: z.string().optional(), + structureSamlauncher: z.string().optional(), + + spriteTransportship: z.string().optional(), + spriteWarship: z.string().optional(), + spriteSammissile: z.string().optional(), + spriteAtombomb: z.string().optional(), + spriteHydrogenbomb: z.string().optional(), + spriteTradeship: z.string().optional(), + spriteMirv: z.string().optional(), + spriteEngine: z.string().optional(), + spriteCarriage: z.string().optional(), + spriteLoadedcarriage: z.string().optional(), +}); +export type PlayerPack = z.infer; + export const PlayerCosmeticsSchema = z.object({ flag: FlagSchema.optional(), pattern: PlayerPatternSchema.optional(), color: PlayerColorSchema.optional(), + pack: PlayerPackSchema.optional(), }); export const PlayerSchema = z.object({ diff --git a/src/core/game/UserSettings.ts b/src/core/game/UserSettings.ts index fd5ac12a5..45b822bb8 100644 --- a/src/core/game/UserSettings.ts +++ b/src/core/game/UserSettings.ts @@ -183,6 +183,70 @@ export class UserSettings { } } + getSelectedStructurePort(): string | undefined { + return localStorage.getItem("structurePort") ?? undefined; + } + + getSelectedStructureCity(): string | undefined { + return localStorage.getItem("structureCity") ?? undefined; + } + + getSelectedStructureFactory(): string | undefined { + return localStorage.getItem("structureFactory") ?? undefined; + } + + getSelectedStructureMissilesilo(): string | undefined { + return localStorage.getItem("structureMissilesilo") ?? undefined; + } + + getSelectedStructureDefensepost(): string | undefined { + return localStorage.getItem("structureDefensepost") ?? undefined; + } + + getSelectedStructureSamlauncher(): string | undefined { + return localStorage.getItem("structureSamlauncher") ?? undefined; + } + + getSelectedSpriteTransportship(): string | undefined { + return localStorage.getItem("spriteTransportship") ?? undefined; + } + + getSelectedSpriteWarship(): string | undefined { + return localStorage.getItem("spriteWarship") ?? undefined; + } + + getSelectedSpriteSammissile(): string | undefined { + return localStorage.getItem("spriteSammissile") ?? undefined; + } + + getSelectedSpriteAtombomb(): string | undefined { + return localStorage.getItem("spriteAtombomb") ?? undefined; + } + + getSelectedSpriteHydrogenbomb(): string | undefined { + return localStorage.getItem("spriteHydrogenbomb") ?? undefined; + } + + getSelectedSpriteTradeship(): string | undefined { + return localStorage.getItem("spriteTradeship") ?? undefined; + } + + getSelectedSpriteMirv(): string | undefined { + return localStorage.getItem("spriteMirv") ?? undefined; + } + + getSelectedSpriteEngine(): string | undefined { + return localStorage.getItem("spriteEngine") ?? undefined; + } + + getSelectedSpriteCarriage(): string | undefined { + return localStorage.getItem("spriteCarriage") ?? undefined; + } + + getSelectedSpriteLoadedcarriage(): string | undefined { + return localStorage.getItem("spriteLoadedcarriage") ?? undefined; + } + backgroundMusicVolume(): number { return this.getFloat("settings.backgroundMusicVolume", 0); } diff --git a/src/server/Privilege.ts b/src/server/Privilege.ts index 380dcfb14..a837be014 100644 --- a/src/server/Privilege.ts +++ b/src/server/Privilege.ts @@ -1,9 +1,11 @@ +import { fetchUrl } from "../client/CosmeticPackLoader"; import { Cosmetics } from "../core/CosmeticSchemas"; import { decodePatternData } from "../core/PatternDecoder"; import { PlayerColor, PlayerCosmeticRefs, PlayerCosmetics, + PlayerPack, PlayerPattern, } from "../core/Schemas"; @@ -42,6 +44,33 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker { } } + const pack = { + structurePort: refs?.structurePort, + structureCity: refs?.structureCity, + structureFactory: refs?.structureFactory, + structureMissilesilo: refs?.structureMissilesilo, + structureDefensepost: refs?.structureDefensepost, + structureSamlauncher: refs?.structureSamlauncher, + spriteTransportship: refs?.spriteTransportship, + spriteWarship: refs?.spriteWarship, + spriteSammissile: refs?.spriteSammissile, + spriteAtombomb: refs?.spriteAtombomb, + spriteHydrogenbomb: refs?.spriteHydrogenbomb, + spriteTradeship: refs?.spriteTradeship, + spriteMirv: refs?.spriteMirv, + spriteEngine: refs?.spriteEngine, + spriteCarriage: refs?.spriteCarriage, + spriteLoadedcarriage: refs?.spriteLoadedcarriage, + }; + + if (Object.values(pack).some((v) => v !== undefined)) { + try { + cosmetics.pack = this.isPackAllowed(flares, pack); + } catch (e) { + return { type: "forbidden", reason: "invalid pack: " + e.message }; + } + } + return { type: "allowed", cosmetics }; } @@ -95,6 +124,46 @@ export class PrivilegeCheckerImpl implements PrivilegeChecker { } return { color }; } + + isPackAllowed(flares: string[], pack: PlayerPack): PlayerPack { + // TODO: add pack privilege checking + return { + structurePort: fetchUrl(pack.structurePort, "structurePort"), + structureCity: fetchUrl(pack.structureCity, "structureCity"), + structureFactory: fetchUrl(pack.structureFactory, "structureFactory"), + structureMissilesilo: fetchUrl( + pack.structureMissilesilo, + "structureMissilesilo", + ), + structureDefensepost: fetchUrl( + pack.structureDefensepost, + "structureDefensepost", + ), + structureSamlauncher: fetchUrl( + pack.structureSamlauncher, + "structureSamlauncher", + ), + spriteTransportship: fetchUrl( + pack.spriteTransportship, + "spriteTransportship", + ), + spriteWarship: fetchUrl(pack.spriteWarship, "spriteWarship"), + spriteSammissile: fetchUrl(pack.spriteSammissile, "spriteSammissile"), + spriteAtombomb: fetchUrl(pack.spriteAtombomb, "spriteAtombomb"), + spriteHydrogenbomb: fetchUrl( + pack.spriteHydrogenbomb, + "spriteHydrogenbomb", + ), + spriteTradeship: fetchUrl(pack.spriteTradeship, "spriteTradeship"), + spriteMirv: fetchUrl(pack.spriteMirv, "spriteMirv"), + spriteEngine: fetchUrl(pack.spriteEngine, "spriteEngine"), + spriteCarriage: fetchUrl(pack.spriteCarriage, "spriteCarriage"), + spriteLoadedcarriage: fetchUrl( + pack.spriteLoadedcarriage, + "spriteLoadedcarriage", + ), + }; + } } export class FailOpenPrivilegeChecker implements PrivilegeChecker {