feat: 명령어 정규화 및 핸들러 매핑 개선
This commit is contained in:
@@ -5,6 +5,7 @@ import type {
|
||||
TurnDaemonStatus,
|
||||
TurnDaemonCommandResult,
|
||||
} from './types.js';
|
||||
import { normalizeTurnDaemonCommand } from '../turn/commandRegistry.js';
|
||||
|
||||
export interface TurnDaemonStreamKeys {
|
||||
commandStream: string;
|
||||
@@ -42,9 +43,6 @@ type TurnDaemonEventEnvelope = {
|
||||
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 => {
|
||||
try {
|
||||
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 {
|
||||
private readonly client: RedisStreamClient;
|
||||
private readonly keys: TurnDaemonStreamKeys;
|
||||
@@ -477,7 +159,7 @@ export class RedisTurnDaemonCommandStream implements TurnDaemonControlQueue, Tur
|
||||
if (!envelope) {
|
||||
continue;
|
||||
}
|
||||
const command = normalizeCommand(envelope);
|
||||
const command = normalizeTurnDaemonCommand(envelope);
|
||||
if (!command) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user