feat: 명령어 정규화 및 핸들러 매핑 개선
This commit is contained in:
@@ -5,6 +5,7 @@ import type {
|
|||||||
TurnDaemonStatus,
|
TurnDaemonStatus,
|
||||||
TurnDaemonCommandResult,
|
TurnDaemonCommandResult,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
import { normalizeTurnDaemonCommand } from '../turn/commandRegistry.js';
|
||||||
|
|
||||||
export interface TurnDaemonStreamKeys {
|
export interface TurnDaemonStreamKeys {
|
||||||
commandStream: string;
|
commandStream: string;
|
||||||
@@ -42,9 +43,6 @@ type TurnDaemonEventEnvelope = {
|
|||||||
event: TurnDaemonEvent;
|
event: TurnDaemonEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
||||||
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
||||||
|
|
||||||
const parseCommandEnvelope = (raw: string): TurnDaemonCommandEnvelope | null => {
|
const parseCommandEnvelope = (raw: string): TurnDaemonCommandEnvelope | null => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw) as Partial<TurnDaemonCommandEnvelope>;
|
const parsed = JSON.parse(raw) as Partial<TurnDaemonCommandEnvelope>;
|
||||||
@@ -66,322 +64,6 @@ const parseCommandEnvelope = (raw: string): TurnDaemonCommandEnvelope | null =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeCommand = (envelope: TurnDaemonCommandEnvelope): TurnDaemonCommand | null => {
|
|
||||||
const command = envelope.command as TurnDaemonCommand & {
|
|
||||||
requestId?: string;
|
|
||||||
};
|
|
||||||
switch (command.type) {
|
|
||||||
case 'auctionFinalize': {
|
|
||||||
if (typeof command.auctionId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'auctionFinalize',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
auctionId: command.auctionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'troopJoin': {
|
|
||||||
if (typeof command.generalId !== 'number' || typeof command.troopId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'troopJoin',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
troopId: command.troopId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'troopExit': {
|
|
||||||
if (typeof command.generalId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'troopExit',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'dieOnPrestart': {
|
|
||||||
if (typeof command.generalId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'dieOnPrestart',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'buildNationCandidate': {
|
|
||||||
if (typeof command.generalId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'buildNationCandidate',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'instantRetreat': {
|
|
||||||
if (typeof command.generalId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'instantRetreat',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'vacation': {
|
|
||||||
if (typeof command.generalId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'vacation',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'setMySetting': {
|
|
||||||
if (typeof command.generalId !== 'number' || !command.settings || typeof command.settings !== 'object') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'setMySetting',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
settings: command.settings,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'dropItem': {
|
|
||||||
if (typeof command.generalId !== 'number' || typeof command.itemType !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'dropItem',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
itemType: command.itemType,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'changePermission': {
|
|
||||||
if (
|
|
||||||
typeof command.generalId !== 'number' ||
|
|
||||||
typeof command.isAmbassador !== 'boolean' ||
|
|
||||||
!Array.isArray(command.targetGeneralIds)
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const targetGeneralIds = command.targetGeneralIds.filter((id) => typeof id === 'number');
|
|
||||||
if (targetGeneralIds.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'changePermission',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
isAmbassador: command.isAmbassador,
|
|
||||||
targetGeneralIds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'kick': {
|
|
||||||
if (typeof command.generalId !== 'number' || typeof command.destGeneralId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'kick',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
destGeneralId: command.destGeneralId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'appoint': {
|
|
||||||
if (
|
|
||||||
typeof command.generalId !== 'number' ||
|
|
||||||
typeof command.destGeneralId !== 'number' ||
|
|
||||||
typeof command.destCityId !== 'number' ||
|
|
||||||
typeof command.officerLevel !== 'number'
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'appoint',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
destGeneralId: command.destGeneralId,
|
|
||||||
destCityId: command.destCityId,
|
|
||||||
officerLevel: command.officerLevel,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'tournamentRefund': {
|
|
||||||
if (!Array.isArray(command.refunds)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const refunds = command.refunds
|
|
||||||
.filter((entry) =>
|
|
||||||
entry && typeof entry.generalId === 'number' && typeof entry.amount === 'number'
|
|
||||||
)
|
|
||||||
.map((entry) => ({
|
|
||||||
generalId: entry.generalId,
|
|
||||||
amount: entry.amount,
|
|
||||||
}));
|
|
||||||
if (refunds.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'tournamentRefund',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
bettingId: typeof command.bettingId === 'number' ? command.bettingId : undefined,
|
|
||||||
reason: typeof command.reason === 'string' ? command.reason : undefined,
|
|
||||||
refunds,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'tournamentBettingPayout': {
|
|
||||||
if (!Array.isArray(command.payouts)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const payouts = command.payouts
|
|
||||||
.filter((entry) =>
|
|
||||||
entry && typeof entry.generalId === 'number' && typeof entry.amount === 'number'
|
|
||||||
)
|
|
||||||
.map((entry) => ({
|
|
||||||
generalId: entry.generalId,
|
|
||||||
amount: entry.amount,
|
|
||||||
}));
|
|
||||||
if (payouts.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'tournamentBettingPayout',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
bettingId: typeof command.bettingId === 'number' ? command.bettingId : undefined,
|
|
||||||
reason: typeof command.reason === 'string' ? command.reason : undefined,
|
|
||||||
payouts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'tournamentReward': {
|
|
||||||
if (typeof command.winnerId !== 'number' || typeof command.runnerUpId !== 'number') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(command.top16) || !Array.isArray(command.top8) || !Array.isArray(command.top4)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const normalizeIds = (list: unknown[]): number[] =>
|
|
||||||
list.filter((entry): entry is number => typeof entry === 'number' && Number.isFinite(entry));
|
|
||||||
const top16 = normalizeIds(command.top16);
|
|
||||||
const top8 = normalizeIds(command.top8);
|
|
||||||
const top4 = normalizeIds(command.top4);
|
|
||||||
if (top16.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'tournamentReward',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
tournamentType: typeof command.tournamentType === 'number' ? command.tournamentType : 0,
|
|
||||||
winnerId: command.winnerId,
|
|
||||||
runnerUpId: command.runnerUpId,
|
|
||||||
top16,
|
|
||||||
top8,
|
|
||||||
top4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'setNationMeta': {
|
|
||||||
if (typeof command.nationId !== 'number' || !command.updates || typeof command.updates !== 'object') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'setNationMeta',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
nationId: command.nationId,
|
|
||||||
updates: command.updates as Record<string, unknown>,
|
|
||||||
expectedUpdatedAt: typeof command.expectedUpdatedAt === 'string' ? command.expectedUpdatedAt : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'adjustGeneralResources': {
|
|
||||||
if (!Array.isArray(command.adjustments)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const adjustments = command.adjustments
|
|
||||||
.filter((entry) => entry && typeof entry.generalId === 'number')
|
|
||||||
.map((entry) => ({
|
|
||||||
generalId: entry.generalId,
|
|
||||||
goldDelta: typeof entry.goldDelta === 'number' ? entry.goldDelta : undefined,
|
|
||||||
riceDelta: typeof entry.riceDelta === 'number' ? entry.riceDelta : undefined,
|
|
||||||
}))
|
|
||||||
.filter((entry) => entry.goldDelta !== undefined || entry.riceDelta !== undefined);
|
|
||||||
if (adjustments.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'adjustGeneralResources',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
reason: typeof command.reason === 'string' ? command.reason : undefined,
|
|
||||||
adjustments,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'patchGeneral': {
|
|
||||||
if (typeof command.generalId !== 'number' || !command.patch || typeof command.patch !== 'object') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'patchGeneral',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
patch: {
|
|
||||||
meta: isRecord(command.patch.meta) ? command.patch.meta : undefined,
|
|
||||||
turnTime: typeof command.patch.turnTime === 'string' ? command.patch.turnTime : undefined,
|
|
||||||
stats: isRecord(command.patch.stats)
|
|
||||||
? {
|
|
||||||
leadership:
|
|
||||||
typeof command.patch.stats.leadership === 'number'
|
|
||||||
? command.patch.stats.leadership
|
|
||||||
: undefined,
|
|
||||||
strength:
|
|
||||||
typeof command.patch.stats.strength === 'number'
|
|
||||||
? command.patch.stats.strength
|
|
||||||
: undefined,
|
|
||||||
intelligence:
|
|
||||||
typeof command.patch.stats.intelligence === 'number'
|
|
||||||
? command.patch.stats.intelligence
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
specialWar: typeof command.patch.specialWar === 'string' ? command.patch.specialWar : undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'auctionBid': {
|
|
||||||
if (
|
|
||||||
typeof command.auctionId !== 'number' ||
|
|
||||||
typeof command.generalId !== 'number' ||
|
|
||||||
typeof command.amount !== 'number'
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'auctionBid',
|
|
||||||
requestId: envelope.requestId,
|
|
||||||
auctionId: command.auctionId,
|
|
||||||
generalId: command.generalId,
|
|
||||||
amount: command.amount,
|
|
||||||
tryExtendCloseDate:
|
|
||||||
typeof command.tryExtendCloseDate === 'boolean' ? command.tryExtendCloseDate : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'getStatus': {
|
|
||||||
const requestId = typeof command.requestId === 'string' ? command.requestId : envelope.requestId;
|
|
||||||
return { type: 'getStatus', requestId };
|
|
||||||
}
|
|
||||||
case 'run':
|
|
||||||
case 'pause':
|
|
||||||
case 'resume':
|
|
||||||
case 'shutdown':
|
|
||||||
return command;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RedisTurnDaemonCommandStream implements TurnDaemonControlQueue, TurnDaemonCommandResponder {
|
export class RedisTurnDaemonCommandStream implements TurnDaemonControlQueue, TurnDaemonCommandResponder {
|
||||||
private readonly client: RedisStreamClient;
|
private readonly client: RedisStreamClient;
|
||||||
private readonly keys: TurnDaemonStreamKeys;
|
private readonly keys: TurnDaemonStreamKeys;
|
||||||
@@ -477,7 +159,7 @@ export class RedisTurnDaemonCommandStream implements TurnDaemonControlQueue, Tur
|
|||||||
if (!envelope) {
|
if (!envelope) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const command = normalizeCommand(envelope);
|
const command = normalizeTurnDaemonCommand(envelope);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
TurnDaemonCommand,
|
||||||
|
TurnDaemonCommandType,
|
||||||
|
TurnDaemonCommandByType,
|
||||||
|
} from '@sammo-ts/common';
|
||||||
|
|
||||||
|
export type TurnDaemonCommandEnvelope = {
|
||||||
|
requestId: string;
|
||||||
|
sentAt: string;
|
||||||
|
command: TurnDaemonCommand;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommandNormalizer<T extends TurnDaemonCommandType> = (
|
||||||
|
envelope: TurnDaemonCommandEnvelope
|
||||||
|
) => TurnDaemonCommandByType<T> | null;
|
||||||
|
|
||||||
|
type CommandNormalizerMap = {
|
||||||
|
[T in TurnDaemonCommandType]?: CommandNormalizer<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseWith = <T>(schema: z.ZodType<T>, value: unknown): T | null => {
|
||||||
|
const parsed = schema.safeParse(value);
|
||||||
|
return parsed.success ? parsed.data : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zFiniteNumber = z.number().finite();
|
||||||
|
const zRecord = z.record(z.string(), z.unknown());
|
||||||
|
|
||||||
|
const zRunReason = z.enum(['schedule', 'manual', 'poke']);
|
||||||
|
const zTurnRunBudget = z.object({
|
||||||
|
budgetMs: z.number().int().positive(),
|
||||||
|
maxGenerals: z.number().int().positive(),
|
||||||
|
catchUpCap: z.number().int().positive(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zAuctionFinalize = z.object({
|
||||||
|
type: z.literal('auctionFinalize'),
|
||||||
|
auctionId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zAuctionBid = z.object({
|
||||||
|
type: z.literal('auctionBid'),
|
||||||
|
auctionId: zFiniteNumber,
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
amount: zFiniteNumber,
|
||||||
|
tryExtendCloseDate: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zTroopJoin = z.object({
|
||||||
|
type: z.literal('troopJoin'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
troopId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zTroopExit = z.object({
|
||||||
|
type: z.literal('troopExit'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zDieOnPrestart = z.object({
|
||||||
|
type: z.literal('dieOnPrestart'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zBuildNationCandidate = z.object({
|
||||||
|
type: z.literal('buildNationCandidate'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zInstantRetreat = z.object({
|
||||||
|
type: z.literal('instantRetreat'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zVacation = z.object({
|
||||||
|
type: z.literal('vacation'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zSetMySetting = z.object({
|
||||||
|
type: z.literal('setMySetting'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
settings: z.object({
|
||||||
|
tnmt: z.number().int().optional(),
|
||||||
|
defence_train: z.number().int().optional(),
|
||||||
|
use_treatment: z.number().int().optional(),
|
||||||
|
use_auto_nation_turn: z.number().int().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zDropItem = z.object({
|
||||||
|
type: z.literal('dropItem'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
itemType: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zChangePermission = z.object({
|
||||||
|
type: z.literal('changePermission'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
isAmbassador: z.boolean(),
|
||||||
|
targetGeneralIds: z.array(zFiniteNumber).min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zKick = z.object({
|
||||||
|
type: z.literal('kick'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
destGeneralId: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zAppoint = z.object({
|
||||||
|
type: z.literal('appoint'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
destGeneralId: zFiniteNumber,
|
||||||
|
destCityId: zFiniteNumber,
|
||||||
|
officerLevel: zFiniteNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zTournamentRefund = z.object({
|
||||||
|
type: z.literal('tournamentRefund'),
|
||||||
|
bettingId: zFiniteNumber.optional(),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
refunds: z.array(z.object({ generalId: zFiniteNumber, amount: zFiniteNumber })).min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zTournamentBettingPayout = z.object({
|
||||||
|
type: z.literal('tournamentBettingPayout'),
|
||||||
|
bettingId: zFiniteNumber.optional(),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
payouts: z.array(z.object({ generalId: zFiniteNumber, amount: zFiniteNumber })).min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zTournamentReward = z.object({
|
||||||
|
type: z.literal('tournamentReward'),
|
||||||
|
tournamentType: zFiniteNumber,
|
||||||
|
winnerId: zFiniteNumber,
|
||||||
|
runnerUpId: zFiniteNumber,
|
||||||
|
top16: z.array(zFiniteNumber).min(1),
|
||||||
|
top8: z.array(zFiniteNumber),
|
||||||
|
top4: z.array(zFiniteNumber),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zSetNationMeta = z.object({
|
||||||
|
type: z.literal('setNationMeta'),
|
||||||
|
nationId: zFiniteNumber,
|
||||||
|
updates: zRecord,
|
||||||
|
expectedUpdatedAt: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zAdjustGeneralResources = z
|
||||||
|
.object({
|
||||||
|
type: z.literal('adjustGeneralResources'),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
adjustments: z
|
||||||
|
.array(
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
goldDelta: zFiniteNumber.optional(),
|
||||||
|
riceDelta: zFiniteNumber.optional(),
|
||||||
|
})
|
||||||
|
.refine((value) => value.goldDelta !== undefined || value.riceDelta !== undefined)
|
||||||
|
)
|
||||||
|
.min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zPatchGeneral = z.object({
|
||||||
|
type: z.literal('patchGeneral'),
|
||||||
|
generalId: zFiniteNumber,
|
||||||
|
patch: z.object({
|
||||||
|
meta: zRecord.optional(),
|
||||||
|
turnTime: z.string().optional(),
|
||||||
|
stats: z
|
||||||
|
.object({
|
||||||
|
leadership: zFiniteNumber.optional(),
|
||||||
|
strength: zFiniteNumber.optional(),
|
||||||
|
intelligence: zFiniteNumber.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
specialWar: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zGetStatus = z.object({
|
||||||
|
type: z.literal('getStatus'),
|
||||||
|
requestId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zRun = z.object({
|
||||||
|
type: z.literal('run'),
|
||||||
|
reason: zRunReason,
|
||||||
|
targetTime: z.string().optional(),
|
||||||
|
budget: zTurnRunBudget.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zPause = z.object({
|
||||||
|
type: z.literal('pause'),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zResume = z.object({
|
||||||
|
type: z.literal('resume'),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zShutdown = z.object({
|
||||||
|
type: z.literal('shutdown'),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeAuctionFinalize: CommandNormalizer<'auctionFinalize'> = (envelope) => {
|
||||||
|
const command = parseWith(zAuctionFinalize, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAuctionBid: CommandNormalizer<'auctionBid'> = (envelope) => {
|
||||||
|
const command = parseWith(zAuctionBid, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTroopJoin: CommandNormalizer<'troopJoin'> = (envelope) => {
|
||||||
|
const command = parseWith(zTroopJoin, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTroopExit: CommandNormalizer<'troopExit'> = (envelope) => {
|
||||||
|
const command = parseWith(zTroopExit, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeDieOnPrestart: CommandNormalizer<'dieOnPrestart'> = (envelope) => {
|
||||||
|
const command = parseWith(zDieOnPrestart, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeBuildNationCandidate: CommandNormalizer<'buildNationCandidate'> = (envelope) => {
|
||||||
|
const command = parseWith(zBuildNationCandidate, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeInstantRetreat: CommandNormalizer<'instantRetreat'> = (envelope) => {
|
||||||
|
const command = parseWith(zInstantRetreat, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeVacation: CommandNormalizer<'vacation'> = (envelope) => {
|
||||||
|
const command = parseWith(zVacation, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeSetMySetting: CommandNormalizer<'setMySetting'> = (envelope) => {
|
||||||
|
const command = parseWith(zSetMySetting, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeDropItem: CommandNormalizer<'dropItem'> = (envelope) => {
|
||||||
|
const command = parseWith(zDropItem, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeChangePermission: CommandNormalizer<'changePermission'> = (envelope) => {
|
||||||
|
const command = parseWith(zChangePermission, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeKick: CommandNormalizer<'kick'> = (envelope) => {
|
||||||
|
const command = parseWith(zKick, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAppoint: CommandNormalizer<'appoint'> = (envelope) => {
|
||||||
|
const command = parseWith(zAppoint, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTournamentRefund: CommandNormalizer<'tournamentRefund'> = (envelope) => {
|
||||||
|
const command = parseWith(zTournamentRefund, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTournamentBettingPayout: CommandNormalizer<'tournamentBettingPayout'> = (envelope) => {
|
||||||
|
const command = parseWith(zTournamentBettingPayout, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTournamentReward: CommandNormalizer<'tournamentReward'> = (envelope) => {
|
||||||
|
const command = parseWith(zTournamentReward, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeSetNationMeta: CommandNormalizer<'setNationMeta'> = (envelope) => {
|
||||||
|
const command = parseWith(zSetNationMeta, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeAdjustGeneralResources: CommandNormalizer<'adjustGeneralResources'> = (envelope) => {
|
||||||
|
const command = parseWith(zAdjustGeneralResources, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizePatchGeneral: CommandNormalizer<'patchGeneral'> = (envelope) => {
|
||||||
|
const command = parseWith(zPatchGeneral, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeGetStatus: CommandNormalizer<'getStatus'> = (envelope) => {
|
||||||
|
const command = parseWith(zGetStatus, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'getStatus',
|
||||||
|
requestId: command.requestId ?? envelope.requestId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRun: CommandNormalizer<'run'> = (envelope) => parseWith(zRun, envelope.command);
|
||||||
|
const normalizePause: CommandNormalizer<'pause'> = (envelope) => parseWith(zPause, envelope.command);
|
||||||
|
const normalizeResume: CommandNormalizer<'resume'> = (envelope) => parseWith(zResume, envelope.command);
|
||||||
|
const normalizeShutdown: CommandNormalizer<'shutdown'> = (envelope) => parseWith(zShutdown, envelope.command);
|
||||||
|
|
||||||
|
const normalizers: CommandNormalizerMap = {
|
||||||
|
auctionFinalize: normalizeAuctionFinalize,
|
||||||
|
auctionBid: normalizeAuctionBid,
|
||||||
|
troopJoin: normalizeTroopJoin,
|
||||||
|
troopExit: normalizeTroopExit,
|
||||||
|
dieOnPrestart: normalizeDieOnPrestart,
|
||||||
|
buildNationCandidate: normalizeBuildNationCandidate,
|
||||||
|
instantRetreat: normalizeInstantRetreat,
|
||||||
|
vacation: normalizeVacation,
|
||||||
|
setMySetting: normalizeSetMySetting,
|
||||||
|
dropItem: normalizeDropItem,
|
||||||
|
changePermission: normalizeChangePermission,
|
||||||
|
kick: normalizeKick,
|
||||||
|
appoint: normalizeAppoint,
|
||||||
|
tournamentRefund: normalizeTournamentRefund,
|
||||||
|
tournamentBettingPayout: normalizeTournamentBettingPayout,
|
||||||
|
tournamentReward: normalizeTournamentReward,
|
||||||
|
setNationMeta: normalizeSetNationMeta,
|
||||||
|
adjustGeneralResources: normalizeAdjustGeneralResources,
|
||||||
|
patchGeneral: normalizePatchGeneral,
|
||||||
|
getStatus: normalizeGetStatus,
|
||||||
|
run: normalizeRun,
|
||||||
|
pause: normalizePause,
|
||||||
|
resume: normalizeResume,
|
||||||
|
shutdown: normalizeShutdown,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeTurnDaemonCommand = (envelope: TurnDaemonCommandEnvelope): TurnDaemonCommand | null => {
|
||||||
|
const commandType = envelope.command.type as TurnDaemonCommandType;
|
||||||
|
const normalizer = normalizers[commandType];
|
||||||
|
if (!normalizer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizer(envelope) as TurnDaemonCommand | null;
|
||||||
|
};
|
||||||
@@ -753,50 +753,37 @@ export const createTurnDaemonCommandHandler = (options: {
|
|||||||
tournamentRewardFinalizer: options.tournamentRewardFinalizer,
|
tournamentRewardFinalizer: options.tournamentRewardFinalizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HandlerMap = Partial<Record<TurnDaemonCommand['type'], (command: TurnDaemonCommand) => Promise<TurnDaemonCommandResult>>>;
|
||||||
|
|
||||||
|
const handlers: HandlerMap = {
|
||||||
|
troopJoin: (command) => handleTroopJoin(ctx, command as Extract<TurnDaemonCommand, { type: 'troopJoin' }>),
|
||||||
|
troopExit: (command) => handleTroopExit(ctx, command as Extract<TurnDaemonCommand, { type: 'troopExit' }>),
|
||||||
|
dieOnPrestart: (command) => handleDieOnPrestart(ctx, command as Extract<TurnDaemonCommand, { type: 'dieOnPrestart' }>),
|
||||||
|
buildNationCandidate: (command) => handleBuildNationCandidate(ctx, command as Extract<TurnDaemonCommand, { type: 'buildNationCandidate' }>),
|
||||||
|
instantRetreat: (command) => handleInstantRetreat(ctx, command as Extract<TurnDaemonCommand, { type: 'instantRetreat' }>),
|
||||||
|
vacation: (command) => handleVacation(ctx, command as Extract<TurnDaemonCommand, { type: 'vacation' }>),
|
||||||
|
setMySetting: (command) => handleSetMySetting(ctx, command as Extract<TurnDaemonCommand, { type: 'setMySetting' }>),
|
||||||
|
dropItem: (command) => handleDropItem(ctx, command as Extract<TurnDaemonCommand, { type: 'dropItem' }>),
|
||||||
|
auctionFinalize: (command) => handleAuctionFinalize(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionFinalize' }>),
|
||||||
|
auctionBid: (command) => handleAuctionBid(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionBid' }>),
|
||||||
|
changePermission: (command) => handleChangePermission(ctx, command as Extract<TurnDaemonCommand, { type: 'changePermission' }>),
|
||||||
|
kick: (command) => handleKick(ctx, command as Extract<TurnDaemonCommand, { type: 'kick' }>),
|
||||||
|
appoint: (command) => handleAppoint(ctx, command as Extract<TurnDaemonCommand, { type: 'appoint' }>),
|
||||||
|
tournamentRefund: (command) => handleTournamentRefund(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentRefund' }>),
|
||||||
|
tournamentBettingPayout: (command) => handleTournamentBettingPayout(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentBettingPayout' }>),
|
||||||
|
tournamentReward: (command) => handleTournamentReward(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentReward' }>),
|
||||||
|
setNationMeta: (command) => handleSetNationMeta(ctx, command as Extract<TurnDaemonCommand, { type: 'setNationMeta' }>),
|
||||||
|
adjustGeneralResources: (command) => handleAdjustGeneralResources(ctx, command as Extract<TurnDaemonCommand, { type: 'adjustGeneralResources' }>),
|
||||||
|
patchGeneral: (command) => handlePatchGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'patchGeneral' }>),
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handle: async (command): Promise<TurnDaemonCommandResult | null> => {
|
handle: async (command): Promise<TurnDaemonCommandResult | null> => {
|
||||||
switch (command.type) {
|
const handler = handlers[command.type];
|
||||||
case 'troopJoin':
|
if (!handler) {
|
||||||
return handleTroopJoin(ctx, command);
|
return null;
|
||||||
case 'troopExit':
|
|
||||||
return handleTroopExit(ctx, command);
|
|
||||||
case 'dieOnPrestart':
|
|
||||||
return handleDieOnPrestart(ctx, command);
|
|
||||||
case 'buildNationCandidate':
|
|
||||||
return handleBuildNationCandidate(ctx, command);
|
|
||||||
case 'instantRetreat':
|
|
||||||
return handleInstantRetreat(ctx, command);
|
|
||||||
case 'vacation':
|
|
||||||
return handleVacation(ctx, command);
|
|
||||||
case 'setMySetting':
|
|
||||||
return handleSetMySetting(ctx, command);
|
|
||||||
case 'dropItem':
|
|
||||||
return handleDropItem(ctx, command);
|
|
||||||
case 'auctionFinalize':
|
|
||||||
return handleAuctionFinalize(ctx, command);
|
|
||||||
case 'auctionBid':
|
|
||||||
return handleAuctionBid(ctx, command);
|
|
||||||
case 'changePermission':
|
|
||||||
return handleChangePermission(ctx, command);
|
|
||||||
case 'kick':
|
|
||||||
return handleKick(ctx, command);
|
|
||||||
case 'appoint':
|
|
||||||
return handleAppoint(ctx, command);
|
|
||||||
case 'tournamentRefund':
|
|
||||||
return handleTournamentRefund(ctx, command);
|
|
||||||
case 'tournamentBettingPayout':
|
|
||||||
return handleTournamentBettingPayout(ctx, command);
|
|
||||||
case 'tournamentReward':
|
|
||||||
return handleTournamentReward(ctx, command);
|
|
||||||
case 'setNationMeta':
|
|
||||||
return handleSetNationMeta(ctx, command);
|
|
||||||
case 'adjustGeneralResources':
|
|
||||||
return handleAdjustGeneralResources(ctx, command);
|
|
||||||
case 'patchGeneral':
|
|
||||||
return handlePatchGeneral(ctx, command);
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return handler(command);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -304,3 +304,12 @@ export type TurnDaemonEvent =
|
|||||||
| { type: 'runCompleted'; at: string; result: TurnRunResult }
|
| { type: 'runCompleted'; at: string; result: TurnRunResult }
|
||||||
| { type: 'runFailed'; at: string; error: string }
|
| { type: 'runFailed'; at: string; error: string }
|
||||||
| { type: 'commandResult'; result: TurnDaemonCommandResult };
|
| { type: 'commandResult'; result: TurnDaemonCommandResult };
|
||||||
|
|
||||||
|
export type TurnDaemonCommandType = TurnDaemonCommand['type'];
|
||||||
|
|
||||||
|
export type TurnDaemonCommandByType<T extends TurnDaemonCommandType> = Extract<TurnDaemonCommand, { type: T }>;
|
||||||
|
|
||||||
|
export type TurnDaemonCommandResultByType<T extends TurnDaemonCommandType> = Extract<
|
||||||
|
TurnDaemonCommandResult,
|
||||||
|
{ type: T }
|
||||||
|
>;
|
||||||
|
|||||||
Reference in New Issue
Block a user