첫 기수 번호를 완료 게임 수의 기준값으로 적용하고 0기를 API와 화면에서 보존한다. 시즌 번호와 독립된 RESET 계약을 관리 패널, 테스트, 운영 문서에 반영한다.
163 lines
6.7 KiB
TypeScript
163 lines
6.7 KiB
TypeScript
import { z } from 'zod';
|
|
import type { ChangeJournal } from '@sammo-ts/common';
|
|
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
|
import type { DatabaseClient as InfraDatabaseClient, RedisConnector, GamePrisma } from '@sammo-ts/infra';
|
|
import { normalizeScenarioEffect, SCENARIO_EFFECT_KEYS } from '@sammo-ts/logic';
|
|
|
|
import type { TurnDaemonTransport } from './daemon/transport.js';
|
|
import type { BattleSimTransport } from './battleSim/transport.js';
|
|
import type { FlushStore } from './auth/flushStore.js';
|
|
import type { RedisAccessTokenStore } from './auth/accessTokenStore.js';
|
|
import type { AccountIconSource } from './auth/accountIconSource.js';
|
|
import type { ProfileStatusSource } from './auth/profileStatusSource.js';
|
|
import type { ContentImageUploadStore } from './services/remoteContentImageStore.js';
|
|
import type { ReadModelOutboxWakeup } from './realtime/outboxWorker.js';
|
|
|
|
export interface GameProfile {
|
|
id: string;
|
|
scenario: string;
|
|
name: string;
|
|
}
|
|
|
|
export const zWorldStateConfig = z.object({
|
|
maxUserCnt: z.number().optional(),
|
|
fictionMode: z.string().optional(),
|
|
fiction: z.number().optional(),
|
|
joinMode: z.string().optional(),
|
|
blockGeneralCreate: z.number().optional(),
|
|
npcMode: z.number().optional(),
|
|
showImgLevel: z.number().optional(),
|
|
tournamentTrig: z.boolean().optional(),
|
|
extendedGeneral: z.boolean().optional(),
|
|
turnTermMinutes: z.number().optional(),
|
|
syncTurnTime: z.boolean().optional(),
|
|
environment: z
|
|
.object({
|
|
scenarioEffect: z
|
|
.union([z.enum(SCENARIO_EFFECT_KEYS), z.literal(''), z.literal('None'), z.null()])
|
|
.transform(normalizeScenarioEffect)
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
});
|
|
export type WorldStateConfig = z.infer<typeof zWorldStateConfig>;
|
|
|
|
export const zWorldStateMeta = z.object({
|
|
serverId: z.string().optional(),
|
|
firstGameIdx: z.number().int().nonnegative().optional(),
|
|
gameIdx: z.number().int().nonnegative().optional(),
|
|
starttime: z.string().optional(),
|
|
opentime: z.string().optional(),
|
|
preopenAt: z.string().optional(),
|
|
turntime: z.string().optional(),
|
|
otherTextInfo: z.string().optional(),
|
|
isUnited: z.number().optional(),
|
|
isunited: z.number().optional(),
|
|
autorun_user: z
|
|
.object({
|
|
limit_minutes: z.number().optional(),
|
|
options: z.record(z.string(), z.boolean()).optional(),
|
|
})
|
|
.nullable()
|
|
.optional(),
|
|
});
|
|
export type WorldStateMeta = z.infer<typeof zWorldStateMeta>;
|
|
|
|
type PrismaWorldStateRow = GamePrisma.WorldStateGetPayload<Record<string, never>>;
|
|
type PrismaGeneralRow = GamePrisma.GeneralGetPayload<Record<string, never>>;
|
|
type WorldClockFields = 'clockBaseTime' | 'clockTick' | 'clockMode' | 'clockWallAnchor' | 'lastTurnTick';
|
|
type GeneralClockFields = 'turnTick' | 'recentWarTick';
|
|
|
|
// Transitional API fixtures may still model the pre-clock row. Runtime Prisma
|
|
// rows always include these nullable columns after migration.
|
|
export type WorldStateRow = Omit<PrismaWorldStateRow, WorldClockFields> &
|
|
Partial<Pick<PrismaWorldStateRow, WorldClockFields>>;
|
|
export type GeneralRow = Omit<PrismaGeneralRow, GeneralClockFields> &
|
|
Partial<Pick<PrismaGeneralRow, GeneralClockFields>>;
|
|
export type GeneralTurnRow = GamePrisma.GeneralTurnGetPayload<Record<string, never>>;
|
|
export type NationTurnRow = GamePrisma.NationTurnGetPayload<Record<string, never>>;
|
|
export type CityRow = GamePrisma.CityGetPayload<Record<string, never>>;
|
|
export type NationRow = GamePrisma.NationGetPayload<Record<string, never>>;
|
|
export type TroopRow = GamePrisma.TroopGetPayload<Record<string, never>>;
|
|
|
|
export type JsonValue = GamePrisma.JsonValue;
|
|
export type JsonObject = GamePrisma.JsonObject;
|
|
export type JsonArray = GamePrisma.JsonArray;
|
|
export type InputJsonValue = GamePrisma.InputJsonValue;
|
|
|
|
export type DatabaseClient = InfraDatabaseClient;
|
|
|
|
export interface GameApiContext {
|
|
requestId?: string;
|
|
generalAccessTracking?: boolean;
|
|
/** Validated server-issued proof for one realtime refresh burst. */
|
|
realtimeAccessGranted?: boolean;
|
|
/** Set only while an API input-event transaction owns the mutation. */
|
|
changeJournal?: ChangeJournal;
|
|
/** Post-commit scheduling hint for the durable outbox dispatcher. */
|
|
readModelOutbox?: ReadModelOutboxWakeup;
|
|
db: DatabaseClient;
|
|
redis: RedisConnector['client'];
|
|
turnDaemon: TurnDaemonTransport;
|
|
battleSim: BattleSimTransport;
|
|
profile: GameProfile;
|
|
uploadDir: string;
|
|
uploadPath: string;
|
|
uploadPublicUrl: string | null;
|
|
contentImageUpload?: ContentImageUploadStore;
|
|
auth: GameSessionTokenPayload | null;
|
|
accessToken?: string;
|
|
accessTokenStore: RedisAccessTokenStore;
|
|
flushStore: FlushStore;
|
|
gameTokenSecret: string;
|
|
accountIconSource?: AccountIconSource;
|
|
// Runtime context always supplies this. Partial router fixtures may omit it;
|
|
// access scoring then fails open and never penalizes a test-only request.
|
|
profileStatusSource?: ProfileStatusSource;
|
|
}
|
|
|
|
export const createGameApiContext = (options: {
|
|
requestId?: string;
|
|
realtimeAccessGranted?: boolean;
|
|
db: DatabaseClient;
|
|
redis: RedisConnector['client'];
|
|
turnDaemon: TurnDaemonTransport;
|
|
battleSim: BattleSimTransport;
|
|
profile: GameProfile;
|
|
uploadDir: string;
|
|
uploadPath: string;
|
|
uploadPublicUrl: string | null;
|
|
contentImageUpload?: ContentImageUploadStore;
|
|
auth: GameSessionTokenPayload | null;
|
|
accessToken?: string;
|
|
accessTokenStore: RedisAccessTokenStore;
|
|
flushStore: FlushStore;
|
|
gameTokenSecret: string;
|
|
accountIconSource?: AccountIconSource;
|
|
profileStatusSource: ProfileStatusSource;
|
|
readModelOutbox?: ReadModelOutboxWakeup;
|
|
}): GameApiContext => {
|
|
return {
|
|
requestId: options.requestId,
|
|
generalAccessTracking: true,
|
|
...(options.realtimeAccessGranted ? { realtimeAccessGranted: true } : {}),
|
|
db: options.db,
|
|
redis: options.redis,
|
|
turnDaemon: options.turnDaemon,
|
|
battleSim: options.battleSim,
|
|
profile: options.profile,
|
|
uploadDir: options.uploadDir,
|
|
uploadPath: options.uploadPath,
|
|
uploadPublicUrl: options.uploadPublicUrl,
|
|
...(options.contentImageUpload ? { contentImageUpload: options.contentImageUpload } : {}),
|
|
auth: options.auth,
|
|
...(options.accessToken ? { accessToken: options.accessToken } : {}),
|
|
accessTokenStore: options.accessTokenStore,
|
|
flushStore: options.flushStore,
|
|
gameTokenSecret: options.gameTokenSecret,
|
|
...(options.accountIconSource ? { accountIconSource: options.accountIconSource } : {}),
|
|
profileStatusSource: options.profileStatusSource,
|
|
...(options.readModelOutbox ? { readModelOutbox: options.readModelOutbox } : {}),
|
|
};
|
|
};
|