feat: refactor utility functions for improved consistency and usability; consolidate parsing logic
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
## Planned Monorepo Layout (TypeScript Rewrite)
|
## Planned Monorepo Layout (TypeScript Rewrite)
|
||||||
|
|
||||||
- `/packages/common`: shared utilities and type definitions.
|
- `/packages/common`: shared utilities and type definitions.
|
||||||
|
- Frequently used utility functions should be registered here and utilized.
|
||||||
- `/packages/infra`: Prisma/Redis connectors and other runtime infra.
|
- `/packages/infra`: Prisma/Redis connectors and other runtime infra.
|
||||||
- `/packages/logic`: pure game logic with DI/interfaces for external dependencies.
|
- `/packages/logic`: pure game logic with DI/interfaces for external dependencies.
|
||||||
- `/app/gateway-frontend`: Gateway UI application.
|
- `/app/gateway-frontend`: Gateway UI application.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { WorldStateRow } from '../context.js';
|
|||||||
import type { BattleSimJobPayload, BattleSimRequestPayload } from './types.js';
|
import type { BattleSimJobPayload, BattleSimRequestPayload } from './types.js';
|
||||||
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
|
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
|
||||||
import type { WarEngineConfig } from '@sammo-ts/logic';
|
import type { WarEngineConfig } from '@sammo-ts/logic';
|
||||||
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
const DEFAULT_WAR_CONFIG = {
|
const DEFAULT_WAR_CONFIG = {
|
||||||
armPerPhase: 500,
|
armPerPhase: 500,
|
||||||
@@ -11,13 +12,6 @@ const DEFAULT_WAR_CONFIG = {
|
|||||||
maxAtmosByWar: 150,
|
maxAtmosByWar: 150,
|
||||||
};
|
};
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> => {
|
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return value as Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = record[key];
|
const value = record[key];
|
||||||
|
|||||||
+10
-15
@@ -1,3 +1,5 @@
|
|||||||
|
import { parseNumberWithFallback } from '@sammo-ts/common';
|
||||||
|
|
||||||
export interface GameApiConfig {
|
export interface GameApiConfig {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -13,17 +15,6 @@ export interface GameApiConfig {
|
|||||||
flushChannel: string;
|
flushChannel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseNumber = (value: string | undefined, fallback: number, label: string): number => {
|
|
||||||
if (!value) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
const parsed = Number(value);
|
|
||||||
if (Number.isNaN(parsed)) {
|
|
||||||
throw new Error(`${label} must be a number.`);
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveGameApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.env): GameApiConfig => {
|
export const resolveGameApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.env): GameApiConfig => {
|
||||||
const profile = env.PROFILE ?? env.SERVER_PROFILE ?? 'hwe';
|
const profile = env.PROFILE ?? env.SERVER_PROFILE ?? 'hwe';
|
||||||
const scenario = env.SCENARIO ?? 'default';
|
const scenario = env.SCENARIO ?? 'default';
|
||||||
@@ -36,19 +27,23 @@ export const resolveGameApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.env
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
host: env.GAME_API_HOST ?? '0.0.0.0',
|
host: env.GAME_API_HOST ?? '0.0.0.0',
|
||||||
port: parseNumber(env.GAME_API_PORT, 14000, 'GAME_API_PORT'),
|
port: parseNumberWithFallback(env.GAME_API_PORT, 14000, 'GAME_API_PORT'),
|
||||||
trpcPath: env.TRPC_PATH ?? '/trpc',
|
trpcPath: env.TRPC_PATH ?? '/trpc',
|
||||||
eventsPath: env.GAME_API_EVENTS_PATH ?? '/events',
|
eventsPath: env.GAME_API_EVENTS_PATH ?? '/events',
|
||||||
profile,
|
profile,
|
||||||
scenario,
|
scenario,
|
||||||
profileName,
|
profileName,
|
||||||
daemonRequestTimeoutMs: parseNumber(env.DAEMON_REQUEST_TIMEOUT_MS, 5000, 'DAEMON_REQUEST_TIMEOUT_MS'),
|
daemonRequestTimeoutMs: parseNumberWithFallback(env.DAEMON_REQUEST_TIMEOUT_MS, 5000, 'DAEMON_REQUEST_TIMEOUT_MS'),
|
||||||
battleSimRequestTimeoutMs: parseNumber(
|
battleSimRequestTimeoutMs: parseNumberWithFallback(
|
||||||
env.BATTLE_SIM_REQUEST_TIMEOUT_MS,
|
env.BATTLE_SIM_REQUEST_TIMEOUT_MS,
|
||||||
8000,
|
8000,
|
||||||
'BATTLE_SIM_REQUEST_TIMEOUT_MS'
|
'BATTLE_SIM_REQUEST_TIMEOUT_MS'
|
||||||
),
|
),
|
||||||
battleSimResultTtlSeconds: parseNumber(env.BATTLE_SIM_RESULT_TTL_SECONDS, 60, 'BATTLE_SIM_RESULT_TTL_SECONDS'),
|
battleSimResultTtlSeconds: parseNumberWithFallback(
|
||||||
|
env.BATTLE_SIM_RESULT_TTL_SECONDS,
|
||||||
|
60,
|
||||||
|
'BATTLE_SIM_RESULT_TTL_SECONDS'
|
||||||
|
),
|
||||||
gameTokenSecret: secret,
|
gameTokenSecret: secret,
|
||||||
flushChannel: `${gatewayPrefix}:flush`,
|
flushChannel: `${gatewayPrefix}:flush`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { GameApiContext, WorldStateRow } from '../context.js';
|
import type { GameApiContext, WorldStateRow } from '../context.js';
|
||||||
|
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export type MapCityCompact = [number, number, number, number, number, number];
|
export type MapCityCompact = [number, number, number, number, number, number];
|
||||||
export type MapNationCompact = [number, string, string, number];
|
export type MapNationCompact = [number, string, string, number];
|
||||||
@@ -45,11 +46,6 @@ const MAP_VERSION = 0 as const;
|
|||||||
const BASE_MAP_TTL_SECONDS = 30;
|
const BASE_MAP_TTL_SECONDS = 30;
|
||||||
const PUBLIC_MAP_TTL_SECONDS = 600;
|
const PUBLIC_MAP_TTL_SECONDS = 600;
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
|
||||||
|
|
||||||
const resolveStartYear = (worldState: WorldStateRow): number => {
|
const resolveStartYear = (worldState: WorldStateRow): number => {
|
||||||
const meta = asRecord(worldState.meta);
|
const meta = asRecord(worldState.meta);
|
||||||
const scenarioMeta = asRecord(meta.scenarioMeta);
|
const scenarioMeta = asRecord(meta.scenarioMeta);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { randomBytes } from 'node:crypto';
|
|||||||
|
|
||||||
import type { WorldStateRow } from '../../context.js';
|
import type { WorldStateRow } from '../../context.js';
|
||||||
import { authedProcedure, router } from '../../trpc.js';
|
import { authedProcedure, router } from '../../trpc.js';
|
||||||
|
import { asNumber, asRecord, asStringArray } from '@sammo-ts/common';
|
||||||
import {
|
import {
|
||||||
isPersonalityTraitKey,
|
isPersonalityTraitKey,
|
||||||
isWarTraitKey,
|
isWarTraitKey,
|
||||||
@@ -23,15 +24,6 @@ const DEFAULT_JOIN_STAT = {
|
|||||||
bonusMax: 5,
|
bonusMax: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
|
||||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
||||||
|
|
||||||
const asNumber = (value: unknown, fallback: number): number =>
|
|
||||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
||||||
|
|
||||||
const asStringArray = (value: unknown): string[] =>
|
|
||||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
|
||||||
|
|
||||||
const resolveJoinStat = (worldState: WorldStateRow) => {
|
const resolveJoinStat = (worldState: WorldStateRow) => {
|
||||||
const config = asRecord(worldState.config);
|
const config = asRecord(worldState.config);
|
||||||
const stat = asRecord(config.stat);
|
const stat = asRecord(config.stat);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
TriggerValue,
|
TriggerValue,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import { evaluateConstraints, loadGeneralTurnCommandSpecs, loadNationTurnCommandSpecs } from '@sammo-ts/logic';
|
import { evaluateConstraints, loadGeneralTurnCommandSpecs, loadNationTurnCommandSpecs } from '@sammo-ts/logic';
|
||||||
|
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
import type { CityRow, GeneralRow, NationRow, WorldStateRow } from '../context.js';
|
import type { CityRow, GeneralRow, NationRow, WorldStateRow } from '../context.js';
|
||||||
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
||||||
@@ -73,11 +74,6 @@ const DEFAULT_GENERAL_GOLD = 1000;
|
|||||||
const DEFAULT_GENERAL_RICE = 1000;
|
const DEFAULT_GENERAL_RICE = 1000;
|
||||||
const DEFAULT_CREW_TYPE_ID = 1100;
|
const DEFAULT_CREW_TYPE_ID = 1100;
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
|
||||||
|
|
||||||
const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
|
const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
|
||||||
isRecord(value) ? (value as Record<string, TriggerValue>) : {};
|
isRecord(value) ? (value as Record<string, TriggerValue>) : {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { DatabaseClient, GeneralTurnRow, NationTurnRow, InputJsonValue } from '../context.js';
|
import type { DatabaseClient, GeneralTurnRow, NationTurnRow, InputJsonValue } from '../context.js';
|
||||||
|
import { isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export const DEFAULT_TURN_ACTION = '휴식';
|
export const DEFAULT_TURN_ACTION = '휴식';
|
||||||
export const MAX_GENERAL_TURNS = 30;
|
export const MAX_GENERAL_TURNS = 30;
|
||||||
@@ -15,9 +16,6 @@ export interface ReservedTurnView {
|
|||||||
args: InputJsonValue;
|
args: InputJsonValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const normalizeAction = (action: string | null | undefined): string =>
|
const normalizeAction = (action: string | null | undefined): string =>
|
||||||
action && action.length > 0 ? action : DEFAULT_TURN_ACTION;
|
action && action.length > 0 ? action : DEFAULT_TURN_ACTION;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import type { City, Nation } from '@sammo-ts/logic';
|
import type { City, Nation } from '@sammo-ts/logic';
|
||||||
|
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
export { asRecord, isRecord };
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
export const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
|
||||||
|
|
||||||
export const readNumber = (value: unknown, fallback = 0): number => {
|
export const readNumber = (value: unknown, fallback = 0): number => {
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { TurnSchedule } from '@sammo-ts/logic';
|
import type { TurnSchedule } from '@sammo-ts/logic';
|
||||||
|
import { parseOptionalBoolean, parseOptionalNumber } from '@sammo-ts/common';
|
||||||
|
|
||||||
import type { TurnRunBudget } from '../lifecycle/types.js';
|
import type { TurnRunBudget } from '../lifecycle/types.js';
|
||||||
import { resolveDatabaseUrl } from '../scenario/databaseUrl.js';
|
import { resolveDatabaseUrl } from '../scenario/databaseUrl.js';
|
||||||
@@ -24,36 +25,11 @@ const DEFAULT_BUDGET: TurnRunBudget = {
|
|||||||
catchUpCap: 1,
|
catchUpCap: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseNumber = (value: string | undefined): number | undefined => {
|
|
||||||
if (!value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const parsed = Number(value);
|
|
||||||
if (!Number.isFinite(parsed)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseBoolean = (value: string | undefined): boolean | undefined => {
|
|
||||||
if (!value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildBudgetOverride = (env: NodeJS.ProcessEnv, override?: Partial<TurnRunBudget>): TurnRunBudget | undefined => {
|
const buildBudgetOverride = (env: NodeJS.ProcessEnv, override?: Partial<TurnRunBudget>): TurnRunBudget | undefined => {
|
||||||
const budgetOverride: Partial<TurnRunBudget> = {
|
const budgetOverride: Partial<TurnRunBudget> = {
|
||||||
budgetMs: parseNumber(env.TURN_BUDGET_MS),
|
budgetMs: parseOptionalNumber(env.TURN_BUDGET_MS),
|
||||||
maxGenerals: parseNumber(env.TURN_MAX_GENERALS),
|
maxGenerals: parseOptionalNumber(env.TURN_MAX_GENERALS),
|
||||||
catchUpCap: parseNumber(env.TURN_CATCH_UP_CAP),
|
catchUpCap: parseOptionalNumber(env.TURN_CATCH_UP_CAP),
|
||||||
...override,
|
...override,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,10 +54,10 @@ export const runTurnDaemonCli = async (options: TurnDaemonCliOptions = {}): Prom
|
|||||||
schema: env.GATEWAY_DB_SCHEMA ?? 'public',
|
schema: env.GATEWAY_DB_SCHEMA ?? 'public',
|
||||||
}));
|
}));
|
||||||
const budget = buildBudgetOverride(env, options.budget);
|
const budget = buildBudgetOverride(env, options.budget);
|
||||||
const tickMinutes = options.tickMinutes ?? parseNumber(env.TURN_TICK_MINUTES);
|
const tickMinutes = options.tickMinutes ?? parseOptionalNumber(env.TURN_TICK_MINUTES);
|
||||||
const enableDatabaseFlush = options.enableDatabaseFlush ?? parseBoolean(env.TURN_FLUSH_DB) ?? true;
|
const enableDatabaseFlush = options.enableDatabaseFlush ?? parseOptionalBoolean(env.TURN_FLUSH_DB) ?? true;
|
||||||
const pauseGateIntervalMs = parseNumber(env.TURN_PAUSE_GATE_MS);
|
const pauseGateIntervalMs = parseOptionalNumber(env.TURN_PAUSE_GATE_MS);
|
||||||
const adminActionIntervalMs = options.adminActionIntervalMs ?? parseNumber(env.TURN_ADMIN_ACTION_MS);
|
const adminActionIntervalMs = options.adminActionIntervalMs ?? parseOptionalNumber(env.TURN_ADMIN_ACTION_MS);
|
||||||
|
|
||||||
const runtime = await createTurnDaemonRuntime({
|
const runtime = await createTurnDaemonRuntime({
|
||||||
profile,
|
profile,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createGatewayPostgresConnector } from '@sammo-ts/infra';
|
import { createGatewayPostgresConnector } from '@sammo-ts/infra';
|
||||||
|
import { isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export type GatewayAdminActionStatus = 'REQUESTED' | 'APPLIED' | 'FAILED' | 'IGNORED';
|
export type GatewayAdminActionStatus = 'REQUESTED' | 'APPLIED' | 'FAILED' | 'IGNORED';
|
||||||
|
|
||||||
@@ -53,9 +54,6 @@ export interface GatewayAdminActionConsumer {
|
|||||||
|
|
||||||
const DEFAULT_POLL_MS = 5000;
|
const DEFAULT_POLL_MS = 5000;
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const normalizeMeta = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
const normalizeMeta = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||||
|
|
||||||
const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
||||||
|
|||||||
@@ -15,16 +15,12 @@ import {
|
|||||||
ITEM_KEYS,
|
ITEM_KEYS,
|
||||||
loadItemModules,
|
loadItemModules,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
const DEFAULT_GENERAL_GOLD = 1000;
|
const DEFAULT_GENERAL_GOLD = 1000;
|
||||||
const DEFAULT_GENERAL_RICE = 1000;
|
const DEFAULT_GENERAL_RICE = 1000;
|
||||||
const DEFAULT_CREW_TYPE_ID = 1100;
|
const DEFAULT_CREW_TYPE_ID = 1100;
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
|
||||||
|
|
||||||
const normalizeCode = (value: string | null | undefined): string | null => {
|
const normalizeCode = (value: string | null | undefined): string | null => {
|
||||||
if (!value || value === 'None') {
|
if (!value || value === 'None') {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
resolveGeneralAction,
|
resolveGeneralAction,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||||
import { LiteHashDRBG } from '@sammo-ts/common';
|
import { asRecord, LiteHashDRBG } from '@sammo-ts/common';
|
||||||
|
|
||||||
import type { ConstraintContext, StateView } from '@sammo-ts/logic';
|
import type { ConstraintContext, StateView } from '@sammo-ts/logic';
|
||||||
|
|
||||||
@@ -43,11 +43,6 @@ import type { AiReservedTurnProvider } from './ai/types.js';
|
|||||||
|
|
||||||
const DEFAULT_ACTION = '휴식';
|
const DEFAULT_ACTION = '휴식';
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
|
||||||
|
|
||||||
const resolveConstraintEnv = (
|
const resolveConstraintEnv = (
|
||||||
world: TurnWorldState,
|
world: TurnWorldState,
|
||||||
scenarioMeta: ScenarioMeta | undefined,
|
scenarioMeta: ScenarioMeta | undefined,
|
||||||
@@ -325,7 +320,7 @@ class WorldStateView implements StateView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractArgsRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
const extractArgsRecord = (value: unknown): Record<string, unknown> => asRecord(value);
|
||||||
|
|
||||||
const buildConstraintContext = (
|
const buildConstraintContext = (
|
||||||
general: TurnGeneral,
|
general: TurnGeneral,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createGamePostgresConnector, type InputJsonValue, type TurnEngineDatabaseClient } from '@sammo-ts/infra';
|
import { createGamePostgresConnector, type InputJsonValue, type TurnEngineDatabaseClient } from '@sammo-ts/infra';
|
||||||
|
import { isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export interface ReservedTurnEntry {
|
export interface ReservedTurnEntry {
|
||||||
action: string;
|
action: string;
|
||||||
@@ -22,9 +23,6 @@ const DEFAULT_NATION_TURNS = 12;
|
|||||||
|
|
||||||
const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;
|
const asJson = (value: unknown): InputJsonValue => value as InputJsonValue;
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const normalizeAction = (action: string | null | undefined): string =>
|
const normalizeAction = (action: string | null | undefined): string =>
|
||||||
action && action.length > 0 ? action : DEFAULT_TURN_ACTION;
|
action && action.length > 0 ? action : DEFAULT_TURN_ACTION;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '@sammo-ts/infra';
|
} from '@sammo-ts/infra';
|
||||||
import type { City, Nation, ScenarioConfig, ScenarioMeta, Troop, TriggerValue } from '@sammo-ts/logic';
|
import type { City, Nation, ScenarioConfig, ScenarioMeta, Troop, TriggerValue } from '@sammo-ts/logic';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
||||||
import type { MapLoaderOptions } from '../scenario/mapLoader.js';
|
import type { MapLoaderOptions } from '../scenario/mapLoader.js';
|
||||||
@@ -27,11 +28,6 @@ interface TurnWorldLoaderOptions {
|
|||||||
|
|
||||||
type JsonRecord = Record<string, unknown>;
|
type JsonRecord = Record<string, unknown>;
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is JsonRecord =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const asRecord = (value: unknown): JsonRecord => (isRecord(value) ? value : {});
|
|
||||||
|
|
||||||
const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
|
const asTriggerRecord = (value: unknown): Record<string, TriggerValue> =>
|
||||||
isRecord(value) ? (value as Record<string, TriggerValue>) : {};
|
isRecord(value) ? (value as Record<string, TriggerValue>) : {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { parseJson } from '@sammo-ts/common';
|
||||||
|
|
||||||
export type OAuthMode = 'login' | 'change_pw';
|
export type OAuthMode = 'login' | 'change_pw';
|
||||||
|
|
||||||
@@ -41,17 +42,6 @@ interface RedisClientLike {
|
|||||||
multi(): RedisPipeline;
|
multi(): RedisPipeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseJson = <T>(raw: string | null): T | null => {
|
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw) as T;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RedisOAuthSessionStore implements OAuthSessionStore {
|
export class RedisOAuthSessionStore implements OAuthSessionStore {
|
||||||
private readonly client: RedisClientLike;
|
private readonly client: RedisClientLike;
|
||||||
private readonly prefix: string;
|
private readonly prefix: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { parseJson } from '@sammo-ts/common';
|
||||||
|
|
||||||
import { createGatewayRedisKeyBuilder } from './redisKeys.js';
|
import { createGatewayRedisKeyBuilder } from './redisKeys.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -30,17 +31,6 @@ interface RedisClientLike {
|
|||||||
del(key: string): Promise<number>;
|
del(key: string): Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseJson = <T>(raw: string | null): T | null => {
|
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(raw) as T;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Redis 세션 저장소는 게이트웨이와 게임 서버 간 SSO 토큰을 관리한다.
|
// Redis 세션 저장소는 게이트웨이와 게임 서버 간 SSO 토큰을 관리한다.
|
||||||
export class RedisGatewaySessionService implements GatewaySessionService {
|
export class RedisGatewaySessionService implements GatewaySessionService {
|
||||||
private readonly client: RedisClientLike;
|
private readonly client: RedisClientLike;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { parseBooleanWithFallback, parseNumberWithFallback } from '@sammo-ts/common';
|
||||||
|
|
||||||
export interface GatewayApiConfig {
|
export interface GatewayApiConfig {
|
||||||
host: string;
|
host: string;
|
||||||
@@ -36,31 +37,6 @@ export interface GatewayOrchestratorConfig {
|
|||||||
worktreeRoot: string;
|
worktreeRoot: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseNumber = (value: string | undefined, fallback: number, label: string): number => {
|
|
||||||
if (!value) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
const parsed = Number(value);
|
|
||||||
if (Number.isNaN(parsed)) {
|
|
||||||
throw new Error(`${label} must be a number.`);
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseBoolean = (value: string | undefined, fallback: boolean): boolean => {
|
|
||||||
if (!value) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
const normalized = value.trim().toLowerCase();
|
|
||||||
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveSchemaName = (value: string | undefined): string => {
|
const resolveSchemaName = (value: string | undefined): string => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return 'public';
|
return 'public';
|
||||||
@@ -83,36 +59,44 @@ export const resolveGatewayApiConfigFromEnv = (env: NodeJS.ProcessEnv = process.
|
|||||||
const redisKeyPrefix = env.GATEWAY_REDIS_PREFIX ?? 'sammo:gateway';
|
const redisKeyPrefix = env.GATEWAY_REDIS_PREFIX ?? 'sammo:gateway';
|
||||||
return {
|
return {
|
||||||
host: env.GATEWAY_API_HOST ?? '0.0.0.0',
|
host: env.GATEWAY_API_HOST ?? '0.0.0.0',
|
||||||
port: parseNumber(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT'),
|
port: parseNumberWithFallback(env.GATEWAY_API_PORT, 13000, 'GATEWAY_API_PORT'),
|
||||||
trpcPath: env.TRPC_PATH ?? '/trpc',
|
trpcPath: env.TRPC_PATH ?? '/trpc',
|
||||||
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
||||||
redisKeyPrefix,
|
redisKeyPrefix,
|
||||||
flushChannel: `${redisKeyPrefix}:flush`,
|
flushChannel: `${redisKeyPrefix}:flush`,
|
||||||
sessionTtlSeconds: parseNumber(env.SESSION_TTL_SECONDS, 60 * 60 * 24 * 7, 'SESSION_TTL_SECONDS'),
|
sessionTtlSeconds: parseNumberWithFallback(env.SESSION_TTL_SECONDS, 60 * 60 * 24 * 7, 'SESSION_TTL_SECONDS'),
|
||||||
gameSessionTtlSeconds: parseNumber(env.GAME_SESSION_TTL_SECONDS, 60 * 60 * 6, 'GAME_SESSION_TTL_SECONDS'),
|
gameSessionTtlSeconds: parseNumberWithFallback(
|
||||||
|
env.GAME_SESSION_TTL_SECONDS,
|
||||||
|
60 * 60 * 6,
|
||||||
|
'GAME_SESSION_TTL_SECONDS'
|
||||||
|
),
|
||||||
gameTokenSecret: secret,
|
gameTokenSecret: secret,
|
||||||
oauthSessionTtlSeconds: parseNumber(env.OAUTH_SESSION_TTL_SECONDS, 10 * 60, 'OAUTH_SESSION_TTL_SECONDS'),
|
oauthSessionTtlSeconds: parseNumberWithFallback(
|
||||||
|
env.OAUTH_SESSION_TTL_SECONDS,
|
||||||
|
10 * 60,
|
||||||
|
'OAUTH_SESSION_TTL_SECONDS'
|
||||||
|
),
|
||||||
kakaoRestKey,
|
kakaoRestKey,
|
||||||
kakaoAdminKey: env.KAKAO_ADMIN_KEY,
|
kakaoAdminKey: env.KAKAO_ADMIN_KEY,
|
||||||
kakaoRedirectUri,
|
kakaoRedirectUri,
|
||||||
publicBaseUrl,
|
publicBaseUrl,
|
||||||
orchestratorEnabled: parseBoolean(env.GATEWAY_ORCHESTRATOR_ENABLED, false),
|
orchestratorEnabled: parseBooleanWithFallback(env.GATEWAY_ORCHESTRATOR_ENABLED, false),
|
||||||
orchestratorReconcileIntervalMs: parseNumber(
|
orchestratorReconcileIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
||||||
15000,
|
15000,
|
||||||
'GATEWAY_ORCHESTRATOR_RECONCILE_MS'
|
'GATEWAY_ORCHESTRATOR_RECONCILE_MS'
|
||||||
),
|
),
|
||||||
orchestratorScheduleIntervalMs: parseNumber(
|
orchestratorScheduleIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_SCHEDULE_MS,
|
env.GATEWAY_ORCHESTRATOR_SCHEDULE_MS,
|
||||||
5000,
|
5000,
|
||||||
'GATEWAY_ORCHESTRATOR_SCHEDULE_MS'
|
'GATEWAY_ORCHESTRATOR_SCHEDULE_MS'
|
||||||
),
|
),
|
||||||
orchestratorBuildIntervalMs: parseNumber(
|
orchestratorBuildIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_BUILD_MS,
|
env.GATEWAY_ORCHESTRATOR_BUILD_MS,
|
||||||
10000,
|
10000,
|
||||||
'GATEWAY_ORCHESTRATOR_BUILD_MS'
|
'GATEWAY_ORCHESTRATOR_BUILD_MS'
|
||||||
),
|
),
|
||||||
orchestratorAdminIntervalMs: parseNumber(
|
orchestratorAdminIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_ADMIN_MS,
|
env.GATEWAY_ORCHESTRATOR_ADMIN_MS,
|
||||||
5000,
|
5000,
|
||||||
'GATEWAY_ORCHESTRATOR_ADMIN_MS'
|
'GATEWAY_ORCHESTRATOR_ADMIN_MS'
|
||||||
@@ -135,22 +119,22 @@ export const resolveGatewayOrchestratorConfigFromEnv = (
|
|||||||
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
dbSchema: resolveSchemaName(env.GATEWAY_DB_SCHEMA),
|
||||||
redisKeyPrefix,
|
redisKeyPrefix,
|
||||||
gameTokenSecret: secret,
|
gameTokenSecret: secret,
|
||||||
orchestratorReconcileIntervalMs: parseNumber(
|
orchestratorReconcileIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
env.GATEWAY_ORCHESTRATOR_RECONCILE_MS,
|
||||||
15000,
|
15000,
|
||||||
'GATEWAY_ORCHESTRATOR_RECONCILE_MS'
|
'GATEWAY_ORCHESTRATOR_RECONCILE_MS'
|
||||||
),
|
),
|
||||||
orchestratorScheduleIntervalMs: parseNumber(
|
orchestratorScheduleIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_SCHEDULE_MS,
|
env.GATEWAY_ORCHESTRATOR_SCHEDULE_MS,
|
||||||
5000,
|
5000,
|
||||||
'GATEWAY_ORCHESTRATOR_SCHEDULE_MS'
|
'GATEWAY_ORCHESTRATOR_SCHEDULE_MS'
|
||||||
),
|
),
|
||||||
orchestratorBuildIntervalMs: parseNumber(
|
orchestratorBuildIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_BUILD_MS,
|
env.GATEWAY_ORCHESTRATOR_BUILD_MS,
|
||||||
10000,
|
10000,
|
||||||
'GATEWAY_ORCHESTRATOR_BUILD_MS'
|
'GATEWAY_ORCHESTRATOR_BUILD_MS'
|
||||||
),
|
),
|
||||||
orchestratorAdminIntervalMs: parseNumber(
|
orchestratorAdminIntervalMs: parseNumberWithFallback(
|
||||||
env.GATEWAY_ORCHESTRATOR_ADMIN_MS,
|
env.GATEWAY_ORCHESTRATOR_ADMIN_MS,
|
||||||
5000,
|
5000,
|
||||||
'GATEWAY_ORCHESTRATOR_ADMIN_MS'
|
'GATEWAY_ORCHESTRATOR_ADMIN_MS'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import { seedScenarioToDatabase, type ScenarioInstallOptions } from '@sammo-ts/game-engine';
|
import { seedScenarioToDatabase, type ScenarioInstallOptions } from '@sammo-ts/game-engine';
|
||||||
import { createGamePostgresConnector, resolvePostgresConfigFromEnv } from '@sammo-ts/infra';
|
import { createGamePostgresConnector, resolvePostgresConfigFromEnv } from '@sammo-ts/infra';
|
||||||
|
import { isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
import type { BuildRunner } from './buildRunner.js';
|
import type { BuildRunner } from './buildRunner.js';
|
||||||
import type { ProcessManager } from './processManager.js';
|
import type { ProcessManager } from './processManager.js';
|
||||||
@@ -103,9 +104,6 @@ interface GatewayAdminActionResult {
|
|||||||
detail?: string;
|
detail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const normalizeMeta = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
const normalizeMeta = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||||
|
|
||||||
const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
const normalizeStatus = (value: unknown): GatewayAdminActionStatus | null => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export * from './util/RNG.js';
|
|||||||
export * from './util/RandUtil.js';
|
export * from './util/RandUtil.js';
|
||||||
export * from './util/TestRNG.js';
|
export * from './util/TestRNG.js';
|
||||||
export * from './util/sha512.js';
|
export * from './util/sha512.js';
|
||||||
|
export * from './util/parse.js';
|
||||||
export * from './turnDaemon/types.js';
|
export * from './turnDaemon/types.js';
|
||||||
export * from './realtime/keys.js';
|
export * from './realtime/keys.js';
|
||||||
export * from './realtime/types.js';
|
export * from './realtime/types.js';
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
export const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||||
|
|
||||||
|
export const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
||||||
|
|
||||||
|
export const asNumber = (value: unknown, fallback: number): number =>
|
||||||
|
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||||
|
|
||||||
|
export const asString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
||||||
|
|
||||||
|
export const asNullableNumber = (value: unknown): number | null =>
|
||||||
|
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
|
|
||||||
|
export const asNullableString = (value: unknown): string | null => (typeof value === 'string' ? value : null);
|
||||||
|
|
||||||
|
export const asStringArray = (value: unknown): string[] =>
|
||||||
|
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||||
|
|
||||||
|
export const asNullableStringArray = (value: unknown): string[] | null => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return asStringArray(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseNumberWithFallback = (value: string | undefined, fallback: number, label?: string): number => {
|
||||||
|
if (!value) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isNaN(parsed)) {
|
||||||
|
if (label) {
|
||||||
|
throw new Error(`${label} must be a number.`);
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseOptionalNumber = (value: string | undefined): number | undefined => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseBooleanWithFallback = (value: string | undefined, fallback: boolean): boolean => {
|
||||||
|
if (!value) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseOptionalBoolean = (value: string | undefined): boolean | undefined => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseJson = <T>(raw: string | null): T | null => {
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@ import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
|||||||
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
||||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||||
import type { ActionContextWorldRef, ActionContextWorldState } from './actionContext.js';
|
import type { ActionContextWorldRef, ActionContextWorldState } from './actionContext.js';
|
||||||
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export interface WorldSummary {
|
export interface WorldSummary {
|
||||||
totalGeneralCount: number;
|
totalGeneralCount: number;
|
||||||
@@ -112,9 +113,6 @@ const DEFAULT_AFTER_CONFIG = {
|
|||||||
defaultCityWall: 1000,
|
defaultCityWall: 1000,
|
||||||
};
|
};
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
|
||||||
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
||||||
|
|
||||||
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const value = record[key];
|
const value = record[key];
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
|
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
|
||||||
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
|
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
|
||||||
|
import { asStringArray, isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export interface TurnCommandProfile {
|
export interface TurnCommandProfile {
|
||||||
general: GeneralTurnCommandKey[];
|
general: GeneralTurnCommandKey[];
|
||||||
nation: NationTurnCommandKey[];
|
nation: NationTurnCommandKey[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
const asStringArrayOrNull = (value: unknown): string[] | null => {
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const asStringArray = (value: unknown): string[] | null => {
|
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const list = value.filter((entry): entry is string => typeof entry === 'string');
|
const list = asStringArray(value);
|
||||||
return list.length > 0 ? list : null;
|
return list.length > 0 ? list : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,7 +21,7 @@ const parseKeyList = <T extends string>(options: {
|
|||||||
isKey: (value: string) => value is T;
|
isKey: (value: string) => value is T;
|
||||||
label: string;
|
label: string;
|
||||||
}): T[] => {
|
}): T[] => {
|
||||||
const rawList = asStringArray(options.raw);
|
const rawList = asStringArrayOrNull(options.raw);
|
||||||
if (!rawList) {
|
if (!rawList) {
|
||||||
return options.defaults;
|
return options.defaults;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { asNullableNumber, asNullableString, asNumber, asString, asStringArray, isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ScenarioConfig,
|
ScenarioConfig,
|
||||||
@@ -23,24 +24,10 @@ const FALLBACK_STAT: ScenarioStatBlock = {
|
|||||||
chiefMin: 0,
|
chiefMin: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is UnknownRecord =>
|
|
||||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
||||||
|
|
||||||
const toRecordOrUndefined = (value: unknown): UnknownRecord | undefined => (isRecord(value) ? value : undefined);
|
const toRecordOrUndefined = (value: unknown): UnknownRecord | undefined => (isRecord(value) ? value : undefined);
|
||||||
|
|
||||||
const toArrayOrUndefined = (value: unknown): unknown[] | undefined => (Array.isArray(value) ? value : undefined);
|
const toArrayOrUndefined = (value: unknown): unknown[] | undefined => (Array.isArray(value) ? value : undefined);
|
||||||
|
|
||||||
const asNumber = (value: unknown, fallback: number): number => (typeof value === 'number' ? value : fallback);
|
|
||||||
|
|
||||||
const asString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
|
||||||
|
|
||||||
const asNullableNumber = (value: unknown): number | null => (typeof value === 'number' ? value : null);
|
|
||||||
|
|
||||||
const asNullableString = (value: unknown): string | null => (typeof value === 'string' ? value : null);
|
|
||||||
|
|
||||||
const asStringArray = (value: unknown): string[] =>
|
|
||||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
|
||||||
|
|
||||||
const zRecord = z.record(z.string(), z.unknown());
|
const zRecord = z.record(z.string(), z.unknown());
|
||||||
const zUnknownArray = z.array(z.unknown());
|
const zUnknownArray = z.array(z.unknown());
|
||||||
const zOptionalRecord = z.preprocess(toRecordOrUndefined, zRecord.optional());
|
const zOptionalRecord = z.preprocess(toRecordOrUndefined, zRecord.optional());
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { isRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
export interface WarDexAux {
|
export interface WarDexAux {
|
||||||
isAttacker?: boolean;
|
isAttacker?: boolean;
|
||||||
opposeType?: { armType: number };
|
opposeType?: { armType: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object';
|
|
||||||
|
|
||||||
export const parseWarDexAux = (aux: unknown): WarDexAux => {
|
export const parseWarDexAux = (aux: unknown): WarDexAux => {
|
||||||
if (!isRecord(aux)) {
|
if (!isRecord(aux)) {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { CrewTypeDefinition, CrewTypeRequirement, MapDefinition, UnitSetDefinition } from './types.js';
|
import type { CrewTypeDefinition, CrewTypeRequirement, MapDefinition, UnitSetDefinition } from './types.js';
|
||||||
|
import {
|
||||||
|
asNullableStringArray,
|
||||||
|
asNumber,
|
||||||
|
asRecord,
|
||||||
|
asString,
|
||||||
|
asStringArray,
|
||||||
|
isRecord,
|
||||||
|
} from '@sammo-ts/common';
|
||||||
|
|
||||||
const DEFAULT_REGION_MAP: Record<string, number> = {
|
const DEFAULT_REGION_MAP: Record<string, number> = {
|
||||||
하북: 1,
|
하북: 1,
|
||||||
@@ -14,26 +22,6 @@ const DEFAULT_REGION_MAP: Record<string, number> = {
|
|||||||
|
|
||||||
const DEFAULT_MAX_TECH_LEVEL = 12;
|
const DEFAULT_MAX_TECH_LEVEL = 12;
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
||||||
|
|
||||||
const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});
|
|
||||||
|
|
||||||
const asNumber = (value: unknown, fallback: number): number =>
|
|
||||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
||||||
|
|
||||||
const asString = (value: unknown, fallback: string): string => (typeof value === 'string' ? value : fallback);
|
|
||||||
|
|
||||||
const asStringArray = (value: unknown): string[] =>
|
|
||||||
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
|
||||||
|
|
||||||
const asNullableStringArray = (value: unknown): string[] | null => {
|
|
||||||
if (value === null || value === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return asStringArray(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeCoef = (value: unknown): Record<string, number> => {
|
const normalizeCoef = (value: unknown): Record<string, number> => {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
Reference in New Issue
Block a user