fix(game): sync diplomatic responses into turn world
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
|
||||||
|
|
||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { asRecord, ChangeJournal } from '@sammo-ts/common';
|
import { asRecord, ChangeJournal } from '@sammo-ts/common';
|
||||||
@@ -14,7 +12,6 @@ import {
|
|||||||
authedProcedure,
|
authedProcedure,
|
||||||
engineAuthedProcedure,
|
engineAuthedProcedure,
|
||||||
router,
|
router,
|
||||||
scopeApiInputEventRequestId,
|
|
||||||
wallAuthedProcedure,
|
wallAuthedProcedure,
|
||||||
} from '../../trpc.js';
|
} from '../../trpc.js';
|
||||||
import {
|
import {
|
||||||
@@ -357,7 +354,7 @@ export const messagesRouter = router({
|
|||||||
let journalPersisted = false;
|
let journalPersisted = false;
|
||||||
const response = await executeInputEvent({
|
const response = await executeInputEvent({
|
||||||
db: ctx.db,
|
db: ctx.db,
|
||||||
requestId: scopeApiInputEventRequestId(ctx.requestId ?? randomUUID(), 'messages.respond.diplomatic', 0),
|
requestId: `messages.respond.diplomatic:${input.messageId}`,
|
||||||
eventType: 'messages.respond.diplomatic',
|
eventType: 'messages.respond.diplomatic',
|
||||||
payload: input,
|
payload: input,
|
||||||
actorUserId: ctx.auth?.user.id,
|
actorUserId: ctx.auth?.user.id,
|
||||||
@@ -393,13 +390,39 @@ export const messagesRouter = router({
|
|||||||
await writeReadModelChangeJournal(transaction, changeJournal.snapshot())
|
await writeReadModelChangeJournal(transaction, changeJournal.snapshot())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return { result: result.result, reason: result.reason };
|
return {
|
||||||
|
result: result.result,
|
||||||
|
reason: result.reason,
|
||||||
|
affectedNationIds: result.affectedNationIds,
|
||||||
|
affectedCityIds: result.affectedCityIds,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (journalPersisted) {
|
if (journalPersisted) {
|
||||||
ctx.readModelOutbox?.wake();
|
ctx.readModelOutbox?.wake();
|
||||||
}
|
}
|
||||||
return response;
|
if (response.result && (response.affectedNationIds.length > 0 || response.affectedCityIds.length > 0)) {
|
||||||
|
if (!ctx.auth) {
|
||||||
|
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
const synchronized = await ctx.turnDaemon.requestCommand({
|
||||||
|
type: 'syncDiplomaticResponse',
|
||||||
|
userId: ctx.auth.user.id,
|
||||||
|
generalId: general.id,
|
||||||
|
messageId: input.messageId,
|
||||||
|
nationIds: response.affectedNationIds,
|
||||||
|
cityIds: response.affectedCityIds,
|
||||||
|
});
|
||||||
|
if (!synchronized || synchronized.type !== 'syncDiplomaticResponse' || !synchronized.ok) {
|
||||||
|
const synchronizationReason =
|
||||||
|
synchronized?.type === 'syncDiplomaticResponse' ? synchronized.reason : undefined;
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'INTERNAL_SERVER_ERROR',
|
||||||
|
message: synchronizationReason ?? '외교 상태를 게임 엔진에 동기화하지 못했습니다.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { result: response.result, reason: response.reason };
|
||||||
}),
|
}),
|
||||||
getOld: authedProcedure
|
getOld: authedProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -959,6 +959,15 @@ describe('messages router missing-flow compatibility', () => {
|
|||||||
const messageActionUpdateMany = vi.fn(async () => ({ count: 1 }));
|
const messageActionUpdateMany = vi.fn(async () => ({ count: 1 }));
|
||||||
const cityUpdate = vi.fn(async () => ({}));
|
const cityUpdate = vi.fn(async () => ({}));
|
||||||
const changeJournal = new ChangeJournal();
|
const changeJournal = new ChangeJournal();
|
||||||
|
const requestCommand = vi.fn(async (command: { type: string; generalId: number; messageId: number }) => ({
|
||||||
|
type: 'syncDiplomaticResponse' as const,
|
||||||
|
ok: true,
|
||||||
|
generalId: command.generalId,
|
||||||
|
messageId: command.messageId,
|
||||||
|
nations: 2,
|
||||||
|
diplomacy: 2,
|
||||||
|
cities: 0,
|
||||||
|
}));
|
||||||
const { caller } = buildContext(
|
const { caller } = buildContext(
|
||||||
{
|
{
|
||||||
general: {
|
general: {
|
||||||
@@ -1049,7 +1058,7 @@ describe('messages router missing-flow compatibility', () => {
|
|||||||
messageAction: { updateMany: messageActionUpdateMany },
|
messageAction: { updateMany: messageActionUpdateMany },
|
||||||
$queryRaw: queryRaw,
|
$queryRaw: queryRaw,
|
||||||
},
|
},
|
||||||
{ changeJournal }
|
{ changeJournal, turnDaemon: { requestCommand } }
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
caller,
|
caller,
|
||||||
@@ -1062,6 +1071,7 @@ describe('messages router missing-flow compatibility', () => {
|
|||||||
messageUpdateMany,
|
messageUpdateMany,
|
||||||
cityUpdate,
|
cityUpdate,
|
||||||
changeJournal,
|
changeJournal,
|
||||||
|
requestCommand,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1075,6 +1085,14 @@ describe('messages router missing-flow compatibility', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toEqual({ result: true, reason: 'success' });
|
expect(result).toEqual({ result: true, reason: 'success' });
|
||||||
|
expect(setup.requestCommand).toHaveBeenCalledWith({
|
||||||
|
type: 'syncDiplomaticResponse',
|
||||||
|
userId: auth.user.id,
|
||||||
|
generalId: setup.actor.id,
|
||||||
|
messageId: 31,
|
||||||
|
nationIds: [1, 2],
|
||||||
|
cityIds: [],
|
||||||
|
});
|
||||||
expect(setup.diplomacyUpdate).toHaveBeenCalledTimes(2);
|
expect(setup.diplomacyUpdate).toHaveBeenCalledTimes(2);
|
||||||
expect(setup.nationUpdate).toHaveBeenCalledWith(
|
expect(setup.nationUpdate).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
|||||||
@@ -131,6 +131,15 @@ const zMessageRespond = z.object({
|
|||||||
response: z.boolean(),
|
response: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const zSyncDiplomaticResponse = z.object({
|
||||||
|
type: z.literal('syncDiplomaticResponse'),
|
||||||
|
userId: z.string().min(1),
|
||||||
|
generalId: z.number().int().positive(),
|
||||||
|
messageId: z.number().int().positive(),
|
||||||
|
nationIds: z.array(z.number().int().positive()).max(4),
|
||||||
|
cityIds: z.array(z.number().int().positive()).max(256),
|
||||||
|
});
|
||||||
|
|
||||||
const zVacation = z.object({
|
const zVacation = z.object({
|
||||||
type: z.literal('vacation'),
|
type: z.literal('vacation'),
|
||||||
userId: z.string().min(1),
|
userId: z.string().min(1),
|
||||||
@@ -609,6 +618,14 @@ const normalizeMessageRespond: CommandNormalizer<'messageRespond'> = (envelope)
|
|||||||
return { ...command, requestId: envelope.requestId };
|
return { ...command, requestId: envelope.requestId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeSyncDiplomaticResponse: CommandNormalizer<'syncDiplomaticResponse'> = (envelope) => {
|
||||||
|
const command = parseWith(zSyncDiplomaticResponse, envelope.command);
|
||||||
|
if (!command) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...command, requestId: envelope.requestId };
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeVacation: CommandNormalizer<'vacation'> = (envelope) => {
|
const normalizeVacation: CommandNormalizer<'vacation'> = (envelope) => {
|
||||||
const command = parseWith(zVacation, envelope.command);
|
const command = parseWith(zVacation, envelope.command);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
@@ -859,6 +876,7 @@ const normalizers: CommandNormalizerMap = {
|
|||||||
buildNationCandidate: normalizeBuildNationCandidate,
|
buildNationCandidate: normalizeBuildNationCandidate,
|
||||||
instantRetreat: normalizeInstantRetreat,
|
instantRetreat: normalizeInstantRetreat,
|
||||||
messageRespond: normalizeMessageRespond,
|
messageRespond: normalizeMessageRespond,
|
||||||
|
syncDiplomaticResponse: normalizeSyncDiplomaticResponse,
|
||||||
vacation: normalizeVacation,
|
vacation: normalizeVacation,
|
||||||
setMySetting: normalizeSetMySetting,
|
setMySetting: normalizeSetMySetting,
|
||||||
dropItem: normalizeDropItem,
|
dropItem: normalizeDropItem,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
normalizeTroopName,
|
normalizeTroopName,
|
||||||
resolveTroopSecretPermission,
|
resolveTroopSecretPermission,
|
||||||
resolveMessageTargetIcon,
|
resolveMessageTargetIcon,
|
||||||
|
readDiplomacyMeta,
|
||||||
type GeneralActionModule,
|
type GeneralActionModule,
|
||||||
rollUniqueLottery,
|
rollUniqueLottery,
|
||||||
type ItemModule,
|
type ItemModule,
|
||||||
@@ -182,6 +183,7 @@ const ACTOR_BOUND_GENERAL_COMMAND_TYPE_LIST = [
|
|||||||
'kick',
|
'kick',
|
||||||
'appoint',
|
'appoint',
|
||||||
'voteReward',
|
'voteReward',
|
||||||
|
'syncDiplomaticResponse',
|
||||||
] as const satisfies readonly TurnDaemonCommand['type'][];
|
] as const satisfies readonly TurnDaemonCommand['type'][];
|
||||||
|
|
||||||
type ActorBoundGeneralCommandType = (typeof ACTOR_BOUND_GENERAL_COMMAND_TYPE_LIST)[number];
|
type ActorBoundGeneralCommandType = (typeof ACTOR_BOUND_GENERAL_COMMAND_TYPE_LIST)[number];
|
||||||
@@ -1839,6 +1841,72 @@ async function handleMessageRespond(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSyncDiplomaticResponse(
|
||||||
|
ctx: CommandHandlerContext,
|
||||||
|
command: Extract<TurnDaemonCommand, { type: 'syncDiplomaticResponse' }>
|
||||||
|
): Promise<TurnDaemonCommandResult> {
|
||||||
|
const db = requireCommandDatabase(ctx);
|
||||||
|
const action = await db.messageAction.findUnique({
|
||||||
|
where: { messageId: command.messageId },
|
||||||
|
select: { status: true },
|
||||||
|
});
|
||||||
|
if (action?.status !== 'RESOLVED') {
|
||||||
|
return {
|
||||||
|
type: 'syncDiplomaticResponse',
|
||||||
|
ok: false,
|
||||||
|
generalId: command.generalId,
|
||||||
|
messageId: command.messageId,
|
||||||
|
nations: 0,
|
||||||
|
diplomacy: 0,
|
||||||
|
cities: 0,
|
||||||
|
reason: '해결되지 않은 외교서신은 동기화할 수 없습니다.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationIds = [...new Set(command.nationIds)];
|
||||||
|
const cityIds = [...new Set(command.cityIds)];
|
||||||
|
const [nations, diplomacy, cities] = await Promise.all([
|
||||||
|
db.nation.findMany({ where: { id: { in: nationIds } }, select: { id: true, meta: true } }),
|
||||||
|
nationIds.length === 0
|
||||||
|
? Promise.resolve([])
|
||||||
|
: db.diplomacy.findMany({
|
||||||
|
where: { srcNationId: { in: nationIds }, destNationId: { in: nationIds } },
|
||||||
|
select: { srcNationId: true, destNationId: true, stateCode: true, term: true, meta: true },
|
||||||
|
}),
|
||||||
|
db.city.findMany({ where: { id: { in: cityIds } }, select: { id: true, frontState: true } }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const nation of nations) {
|
||||||
|
ctx.world.updateNation(nation.id, { meta: asRecord(nation.meta) as Record<string, TriggerValue> });
|
||||||
|
}
|
||||||
|
for (const entry of diplomacy) {
|
||||||
|
const parsedMeta = readDiplomacyMeta(asRecord(entry.meta));
|
||||||
|
ctx.world.applyDiplomacyPatch({
|
||||||
|
srcNationId: entry.srcNationId,
|
||||||
|
destNationId: entry.destNationId,
|
||||||
|
patch: {
|
||||||
|
state: entry.stateCode,
|
||||||
|
term: entry.term,
|
||||||
|
dead: parsedMeta.dead,
|
||||||
|
meta: parsedMeta.meta as Record<string, TriggerValue>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const city of cities) {
|
||||||
|
ctx.world.updateCity(city.id, { frontState: city.frontState });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'syncDiplomaticResponse',
|
||||||
|
ok: true,
|
||||||
|
generalId: command.generalId,
|
||||||
|
messageId: command.messageId,
|
||||||
|
nations: nations.length,
|
||||||
|
diplomacy: diplomacy.length,
|
||||||
|
cities: cities.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function handleVacation(
|
async function handleVacation(
|
||||||
ctx: CommandHandlerContext,
|
ctx: CommandHandlerContext,
|
||||||
command: Extract<TurnDaemonCommand, { type: 'vacation' }>
|
command: Extract<TurnDaemonCommand, { type: 'vacation' }>
|
||||||
@@ -3151,6 +3219,11 @@ export const createTurnDaemonCommandHandler = (options: {
|
|||||||
handleInstantRetreat(ctx, command as Extract<TurnDaemonCommand, { type: 'instantRetreat' }>),
|
handleInstantRetreat(ctx, command as Extract<TurnDaemonCommand, { type: 'instantRetreat' }>),
|
||||||
messageRespond: (command) =>
|
messageRespond: (command) =>
|
||||||
handleMessageRespond(ctx, command as Extract<TurnDaemonCommand, { type: 'messageRespond' }>),
|
handleMessageRespond(ctx, command as Extract<TurnDaemonCommand, { type: 'messageRespond' }>),
|
||||||
|
syncDiplomaticResponse: (command) =>
|
||||||
|
handleSyncDiplomaticResponse(
|
||||||
|
ctx,
|
||||||
|
command as Extract<TurnDaemonCommand, { type: 'syncDiplomaticResponse' }>
|
||||||
|
),
|
||||||
vacation: (command) => handleVacation(ctx, command as Extract<TurnDaemonCommand, { type: 'vacation' }>),
|
vacation: (command) => handleVacation(ctx, command as Extract<TurnDaemonCommand, { type: 'vacation' }>),
|
||||||
setMySetting: (command) =>
|
setMySetting: (command) =>
|
||||||
handleSetMySetting(ctx, command as Extract<TurnDaemonCommand, { type: 'setMySetting' }>),
|
handleSetMySetting(ctx, command as Extract<TurnDaemonCommand, { type: 'setMySetting' }>),
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ const buildActorBoundCommands = (userId = 'old-owner'): TurnDaemonCommand[] => [
|
|||||||
officerLevel: 4,
|
officerLevel: 4,
|
||||||
},
|
},
|
||||||
{ type: 'voteReward', requestId: 'voteReward', userId, generalId: 7, voteId: 1, selection: [0] },
|
{ type: 'voteReward', requestId: 'voteReward', userId, generalId: 7, voteId: 1, selection: [0] },
|
||||||
|
{
|
||||||
|
type: 'syncDiplomaticResponse',
|
||||||
|
requestId: 'syncDiplomaticResponse',
|
||||||
|
userId,
|
||||||
|
generalId: 7,
|
||||||
|
messageId: 31,
|
||||||
|
nationIds: [1, 2],
|
||||||
|
cityIds: [1],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const buildReadOnlyWorld = (ownerUserId: string) => {
|
const buildReadOnlyWorld = (ownerUserId: string) => {
|
||||||
@@ -181,6 +190,66 @@ describe('authenticated actor-bound command registry and execution', () => {
|
|||||||
expect(mutation).not.toHaveBeenCalled();
|
expect(mutation).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('refreshes the daemon world from the committed diplomatic response before the next turn', async () => {
|
||||||
|
const updateNation = vi.fn();
|
||||||
|
const applyDiplomacyPatch = vi.fn();
|
||||||
|
const updateCity = vi.fn();
|
||||||
|
const world = {
|
||||||
|
getGeneralById: vi.fn(() => ({ id: 7, userId: 'old-owner' })),
|
||||||
|
updateNation,
|
||||||
|
applyDiplomacyPatch,
|
||||||
|
updateCity,
|
||||||
|
} as unknown as InMemoryTurnWorld;
|
||||||
|
const db = {
|
||||||
|
inputEvent: {
|
||||||
|
findUnique: vi.fn(async () => ({
|
||||||
|
actorUserId: 'old-owner',
|
||||||
|
target: 'ENGINE',
|
||||||
|
eventType: 'syncDiplomaticResponse',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
messageAction: { findUnique: vi.fn(async () => ({ status: 'RESOLVED' })) },
|
||||||
|
nation: { findMany: vi.fn(async () => [{ id: 1, meta: { policy: 'balanced' } }]) },
|
||||||
|
diplomacy: {
|
||||||
|
findMany: vi.fn(async () => [
|
||||||
|
{ srcNationId: 1, destNationId: 2, stateCode: 7, term: 12, meta: { dead: 3 } },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
city: { findMany: vi.fn(async () => [{ id: 4, frontState: 2 }]) },
|
||||||
|
};
|
||||||
|
const handler = createTurnDaemonCommandHandler({ world });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
handler.handle(
|
||||||
|
{
|
||||||
|
type: 'syncDiplomaticResponse',
|
||||||
|
requestId: 'syncDiplomaticResponse',
|
||||||
|
userId: 'old-owner',
|
||||||
|
generalId: 7,
|
||||||
|
messageId: 31,
|
||||||
|
nationIds: [1, 2],
|
||||||
|
cityIds: [4],
|
||||||
|
},
|
||||||
|
{ db: db as never }
|
||||||
|
)
|
||||||
|
).resolves.toEqual({
|
||||||
|
type: 'syncDiplomaticResponse',
|
||||||
|
ok: true,
|
||||||
|
generalId: 7,
|
||||||
|
messageId: 31,
|
||||||
|
nations: 1,
|
||||||
|
diplomacy: 1,
|
||||||
|
cities: 1,
|
||||||
|
});
|
||||||
|
expect(updateNation).toHaveBeenCalledWith(1, { meta: { policy: 'balanced' } });
|
||||||
|
expect(applyDiplomacyPatch).toHaveBeenCalledWith({
|
||||||
|
srcNationId: 1,
|
||||||
|
destNationId: 2,
|
||||||
|
patch: { state: 7, term: 12, dead: 3, meta: {} },
|
||||||
|
});
|
||||||
|
expect(updateCity).toHaveBeenCalledWith(4, { frontState: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves direct in-memory invocation when no command database is supplied', async () => {
|
it('preserves direct in-memory invocation when no command database is supplied', async () => {
|
||||||
const updateGeneral = vi.fn();
|
const updateGeneral = vi.fn();
|
||||||
const world = {
|
const world = {
|
||||||
|
|||||||
@@ -161,6 +161,15 @@ export type TurnDaemonCommand =
|
|||||||
messageId: number;
|
messageId: number;
|
||||||
response: boolean;
|
response: boolean;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'syncDiplomaticResponse';
|
||||||
|
requestId?: string;
|
||||||
|
userId: string;
|
||||||
|
generalId: number;
|
||||||
|
messageId: number;
|
||||||
|
nationIds: number[];
|
||||||
|
cityIds: number[];
|
||||||
|
}
|
||||||
| { type: 'vacation'; requestId?: string; userId: string; generalId: number }
|
| { type: 'vacation'; requestId?: string; userId: string; generalId: number }
|
||||||
| {
|
| {
|
||||||
type: 'setMySetting';
|
type: 'setMySetting';
|
||||||
@@ -575,6 +584,16 @@ export type TurnDaemonCommandResult =
|
|||||||
action?: 'scout' | 'raiseInvader';
|
action?: 'scout' | 'raiseInvader';
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'syncDiplomaticResponse';
|
||||||
|
ok: boolean;
|
||||||
|
generalId: number;
|
||||||
|
messageId: number;
|
||||||
|
nations: number;
|
||||||
|
diplomacy: number;
|
||||||
|
cities: number;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
| { type: 'vacation'; ok: boolean; generalId: number; reason?: string }
|
| { type: 'vacation'; ok: boolean; generalId: number; reason?: string }
|
||||||
| { type: 'setMySetting'; ok: boolean; generalId: number; reason?: string }
|
| { type: 'setMySetting'; ok: boolean; generalId: number; reason?: string }
|
||||||
| { type: 'dropItem'; ok: boolean; generalId: number; reason?: string }
|
| { type: 'dropItem'; ok: boolean; generalId: number; reason?: string }
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ type State = {
|
|||||||
};
|
};
|
||||||
actionableMessages?: { recruitmentId: number; noAggressionId: number };
|
actionableMessages?: { recruitmentId: number; noAggressionId: number };
|
||||||
cancelNoAggressionMessageId?: number;
|
cancelNoAggressionMessageId?: number;
|
||||||
|
pausedTournamentBet?: {
|
||||||
|
bettorGeneralId: number;
|
||||||
|
targetGeneralId: number;
|
||||||
|
preparedGameTick: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const log = (event: string, detail: Record<string, unknown> = {}): void => {
|
const log = (event: string, detail: Record<string, unknown> = {}): void => {
|
||||||
@@ -832,14 +837,17 @@ const prepareActionFixture = async (): Promise<void> => {
|
|||||||
const fixture = await db.prisma.$transaction(async (tx) => {
|
const fixture = await db.prisma.$transaction(async (tx) => {
|
||||||
const world = await tx.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
const world = await tx.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||||
if (world.clockPhase !== 'SUSPENDED') {
|
if (world.clockPhase !== 'SUSPENDED') {
|
||||||
throw new Error(`Action fixture requires a stopped or paused SUSPENDED clock, found ${world.clockPhase}.`);
|
throw new Error(
|
||||||
|
`Action fixture requires a stopped or paused SUSPENDED clock, found ${world.clockPhase}.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const nations = await tx.nation.findMany({
|
const nations = await tx.nation.findMany({
|
||||||
where: { id: { gt: 0 }, level: { gt: 0 } },
|
where: { id: { gt: 0 }, level: { gt: 0 } },
|
||||||
orderBy: [{ level: 'desc' }, { id: 'asc' }],
|
orderBy: [{ level: 'desc' }, { id: 'asc' }],
|
||||||
take: 2,
|
take: 2,
|
||||||
});
|
});
|
||||||
if (nations.length !== 2) throw new Error(`Action fixture requires two active nations, found ${nations.length}.`);
|
if (nations.length !== 2)
|
||||||
|
throw new Error(`Action fixture requires two active nations, found ${nations.length}.`);
|
||||||
const generalRows = await tx.general.findMany({
|
const generalRows = await tx.general.findMany({
|
||||||
where: { name: { in: state.users!.map((user) => user.generalName) }, userId: { not: null } },
|
where: { name: { in: state.users!.map((user) => user.generalName) }, userId: { not: null } },
|
||||||
orderBy: { id: 'asc' },
|
orderBy: { id: 'asc' },
|
||||||
@@ -921,7 +929,8 @@ const prepareActionFixture = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotateFirst = <T>(values: readonly T[]): T[] => (values.length < 2 ? [...values] : [...values.slice(1), values[0]!]);
|
const rotateFirst = <T>(values: readonly T[]): T[] =>
|
||||||
|
values.length < 2 ? [...values] : [...values.slice(1), values[0]!];
|
||||||
|
|
||||||
const readHiddenBuffLevel = (rawMeta: unknown, key: string): number => {
|
const readHiddenBuffLevel = (rawMeta: unknown, key: string): number => {
|
||||||
if (!rawMeta || typeof rawMeta !== 'object' || Array.isArray(rawMeta)) return 0;
|
if (!rawMeta || typeof rawMeta !== 'object' || Array.isArray(rawMeta)) return 0;
|
||||||
@@ -993,7 +1002,8 @@ const exercisePausedActions = async (): Promise<void> => {
|
|||||||
if (worldBefore.clockPhase !== 'SUSPENDED') {
|
if (worldBefore.clockPhase !== 'SUSPENDED') {
|
||||||
throw new Error(`Paused action matrix requires SUSPENDED, found ${worldBefore.clockPhase}.`);
|
throw new Error(`Paused action matrix requires SUSPENDED, found ${worldBefore.clockPhase}.`);
|
||||||
}
|
}
|
||||||
if (worldBefore.clockTick === null) throw new Error('Paused action matrix requires an authoritative clock tick.');
|
if (worldBefore.clockTick === null)
|
||||||
|
throw new Error('Paused action matrix requires an authoritative clock tick.');
|
||||||
const clients = state.users.map((user) => createGame(user.gameToken));
|
const clients = state.users.map((user) => createGame(user.gameToken));
|
||||||
const generals = state.actionFixture.generals;
|
const generals = state.actionFixture.generals;
|
||||||
const firstNation = state.actionFixture.nations[0]!;
|
const firstNation = state.actionFixture.nations[0]!;
|
||||||
@@ -1050,8 +1060,7 @@ const exercisePausedActions = async (): Promise<void> => {
|
|||||||
});
|
});
|
||||||
const inheritanceById = new Map(inheritanceRows.map((general) => [general.id, general.meta]));
|
const inheritanceById = new Map(inheritanceRows.map((general) => [general.id, general.meta]));
|
||||||
const inheritanceGeneralIndex = generals.findIndex(
|
const inheritanceGeneralIndex = generals.findIndex(
|
||||||
(general, index) =>
|
(general, index) => index >= 3 && readHiddenBuffLevel(inheritanceById.get(general.id), 'warAvoidRatio') < 1
|
||||||
index >= 3 && readHiddenBuffLevel(inheritanceById.get(general.id), 'warAvoidRatio') < 1
|
|
||||||
);
|
);
|
||||||
if (inheritanceGeneralIndex < 0) throw new Error('No lifecycle user remains for the inheritance purchase.');
|
if (inheritanceGeneralIndex < 0) throw new Error('No lifecycle user remains for the inheritance purchase.');
|
||||||
const inheritance = await clients[inheritanceGeneralIndex]!.inherit.buyHiddenBuff.mutate({
|
const inheritance = await clients[inheritanceGeneralIndex]!.inherit.buyHiddenBuff.mutate({
|
||||||
@@ -1095,11 +1104,22 @@ const exercisePausedActions = async (): Promise<void> => {
|
|||||||
db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } }),
|
db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } }),
|
||||||
db.prisma.general.findMany({
|
db.prisma.general.findMany({
|
||||||
where: { id: { in: [generals[2]!.id, generals[4]!.id] } },
|
where: { id: { in: [generals[2]!.id, generals[4]!.id] } },
|
||||||
select: { id: true, nationId: true, cityId: true, officerLevel: true, weaponCode: true, meta: true },
|
select: {
|
||||||
|
id: true,
|
||||||
|
nationId: true,
|
||||||
|
cityId: true,
|
||||||
|
officerLevel: true,
|
||||||
|
weaponCode: true,
|
||||||
|
meta: true,
|
||||||
|
},
|
||||||
orderBy: { id: 'asc' },
|
orderBy: { id: 'asc' },
|
||||||
}),
|
}),
|
||||||
db.prisma.message.findMany({
|
db.prisma.message.findMany({
|
||||||
where: { id: { in: [publicMessage.msgId, privateMessage.msgId, nationalMessage.msgId, expiryMessage.msgId] } },
|
where: {
|
||||||
|
id: {
|
||||||
|
in: [publicMessage.msgId, privateMessage.msgId, nationalMessage.msgId, expiryMessage.msgId],
|
||||||
|
},
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
time: true,
|
time: true,
|
||||||
@@ -1122,7 +1142,9 @@ const exercisePausedActions = async (): Promise<void> => {
|
|||||||
]);
|
]);
|
||||||
if (worldAfter.clockTick === null) throw new Error('Paused action matrix lost the authoritative clock tick.');
|
if (worldAfter.clockTick === null) throw new Error('Paused action matrix lost the authoritative clock tick.');
|
||||||
if (worldAfter.clockTick !== worldBefore.clockTick) {
|
if (worldAfter.clockTick !== worldBefore.clockTick) {
|
||||||
throw new Error(`Game tick moved during paused actions: ${worldBefore.clockTick} -> ${worldAfter.clockTick}.`);
|
throw new Error(
|
||||||
|
`Game tick moved during paused actions: ${worldBefore.clockTick} -> ${worldAfter.clockTick}.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (persistedMessages.some((message) => message.timeTick !== null)) {
|
if (persistedMessages.some((message) => message.timeTick !== null)) {
|
||||||
throw new Error('A paused ordinary message unexpectedly persisted a GAME_TIME occurrence tick.');
|
throw new Error('A paused ordinary message unexpectedly persisted a GAME_TIME occurrence tick.');
|
||||||
@@ -1223,7 +1245,9 @@ const verifyPausedWallExpiry = async (): Promise<void> => {
|
|||||||
rejectedMessage = error instanceof Error ? error.message : String(error);
|
rejectedMessage = error instanceof Error ? error.message : String(error);
|
||||||
}
|
}
|
||||||
if (!rejectedMessage.includes('5분 이내')) {
|
if (!rejectedMessage.includes('5분 이내')) {
|
||||||
throw new Error(`Expired paused message was not rejected by the wall window: ${rejectedMessage || 'accepted'}`);
|
throw new Error(
|
||||||
|
`Expired paused message was not rejected by the wall window: ${rejectedMessage || 'accepted'}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const [after, world] = await Promise.all([
|
const [after, world] = await Promise.all([
|
||||||
db.prisma.message.findUniqueOrThrow({
|
db.prisma.message.findUniqueOrThrow({
|
||||||
@@ -1252,7 +1276,7 @@ const verifyPausedWallExpiry = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const exercisePausedTournamentBet = async (): Promise<void> => {
|
const prepareTournamentBet = async (): Promise<void> => {
|
||||||
const state = await readState();
|
const state = await readState();
|
||||||
if (state.users?.length !== 10 || !state.actionFixture) {
|
if (state.users?.length !== 10 || !state.actionFixture) {
|
||||||
throw new Error('Ten users and the action fixture are required.');
|
throw new Error('Ten users and the action fixture are required.');
|
||||||
@@ -1261,8 +1285,8 @@ const exercisePausedTournamentBet = async (): Promise<void> => {
|
|||||||
await db.connect();
|
await db.connect();
|
||||||
try {
|
try {
|
||||||
const world = await db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
const world = await db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||||
if (world.clockPhase !== 'SUSPENDED' || world.clockTick === null || !world.clockWallAnchor) {
|
if (!['RUNNING', 'MANUAL'].includes(world.clockPhase) || world.clockTick === null || !world.clockWallAnchor) {
|
||||||
throw new Error('Paused tournament betting requires a suspended authoritative game clock.');
|
throw new Error(`Tournament setup requires a running authoritative game clock, found ${world.clockPhase}.`);
|
||||||
}
|
}
|
||||||
const bettor = state.actionFixture.generals[6]!;
|
const bettor = state.actionFixture.generals[6]!;
|
||||||
const target = state.actionFixture.generals[7]!;
|
const target = state.actionFixture.generals[7]!;
|
||||||
@@ -1296,6 +1320,44 @@ const exercisePausedTournamentBet = async (): Promise<void> => {
|
|||||||
bettingSettled: false,
|
bettingSettled: false,
|
||||||
rewardSettled: false,
|
rewardSettled: false,
|
||||||
});
|
});
|
||||||
|
state.pausedTournamentBet = {
|
||||||
|
bettorGeneralId: bettor.id,
|
||||||
|
targetGeneralId: target.id,
|
||||||
|
preparedGameTick: world.clockTick.toString(),
|
||||||
|
};
|
||||||
|
await writeState(state);
|
||||||
|
log('tournament-bet-prepared', {
|
||||||
|
bettorGeneralId: bettor.id,
|
||||||
|
targetGeneralId: target.id,
|
||||||
|
opponentGeneralId: opponent.id,
|
||||||
|
preparedGameTick: world.clockTick.toString(),
|
||||||
|
bettingCloseAt: deadline,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await db.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPausedTournamentBet = async (): Promise<void> => {
|
||||||
|
const state = await readState();
|
||||||
|
if (state.users?.length !== 10 || !state.actionFixture || !state.pausedTournamentBet) {
|
||||||
|
throw new Error('A running-clock tournament fixture is required before the paused bet.');
|
||||||
|
}
|
||||||
|
const db = createGamePostgresConnector({ url: gameDatabaseUrl() });
|
||||||
|
await db.connect();
|
||||||
|
try {
|
||||||
|
const world = await db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||||
|
if (world.clockPhase !== 'SUSPENDED' || world.clockTick === null) {
|
||||||
|
throw new Error(`Paused tournament betting requires SUSPENDED, found ${world.clockPhase}.`);
|
||||||
|
}
|
||||||
|
const bettor = state.actionFixture.generals[6]!;
|
||||||
|
const target = state.actionFixture.generals[7]!;
|
||||||
|
if (
|
||||||
|
bettor.id !== state.pausedTournamentBet.bettorGeneralId ||
|
||||||
|
target.id !== state.pausedTournamentBet.targetGeneralId
|
||||||
|
) {
|
||||||
|
throw new Error('Tournament bettor fixture no longer matches the persisted lifecycle state.');
|
||||||
|
}
|
||||||
const before = await db.prisma.general.findUniqueOrThrow({ where: { id: bettor.id }, select: { gold: true } });
|
const before = await db.prisma.general.findUniqueOrThrow({ where: { id: bettor.id }, select: { gold: true } });
|
||||||
const result = await createGame(state.users[6]!.gameToken).tournament.placeBet.mutate({
|
const result = await createGame(state.users[6]!.gameToken).tournament.placeBet.mutate({
|
||||||
targetId: target.id,
|
targetId: target.id,
|
||||||
@@ -1322,6 +1384,7 @@ const exercisePausedTournamentBet = async (): Promise<void> => {
|
|||||||
goldAfter: after.gold,
|
goldAfter: after.gold,
|
||||||
betCount: snapshot.betCount,
|
betCount: snapshot.betCount,
|
||||||
clockTick: world.clockTick.toString(),
|
clockTick: world.clockTick.toString(),
|
||||||
|
preparedGameTick: state.pausedTournamentBet.preparedGameTick,
|
||||||
result,
|
result,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1467,21 +1530,36 @@ const respondActionableMessages = async (): Promise<void> => {
|
|||||||
await db.connect();
|
await db.connect();
|
||||||
try {
|
try {
|
||||||
const worldBefore = await db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
const worldBefore = await db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||||
if (worldBefore.clockPhase !== 'SUSPENDED' || worldBefore.clockTick === null) {
|
if (!['RUNNING', 'MANUAL'].includes(worldBefore.clockPhase) || worldBefore.clockTick === null) {
|
||||||
throw new Error(`Paused actionable response requires SUSPENDED, found ${worldBefore.clockPhase}.`);
|
throw new Error(`Actionable response requires a running game clock, found ${worldBefore.clockPhase}.`);
|
||||||
}
|
}
|
||||||
const recruitmentTarget = state.actionFixture.generals[9]!;
|
const recruitmentTarget = state.actionFixture.generals[9]!;
|
||||||
const diplomacyActor = state.actionFixture.generals[5]!;
|
const diplomacyActor = state.actionFixture.generals[5]!;
|
||||||
const recruitment = await createGame(state.users[9]!.gameToken).messages.respond.mutate({
|
const actionsBefore = await db.prisma.messageAction.findMany({
|
||||||
|
where: { messageId: { in: Object.values(state.actionableMessages) } },
|
||||||
|
});
|
||||||
|
const recruitment =
|
||||||
|
actionsBefore.find((action) => action.messageId === state.actionableMessages!.recruitmentId)?.status ===
|
||||||
|
'PENDING'
|
||||||
|
? await createGame(state.users[9]!.gameToken).messages.respond.mutate({
|
||||||
generalId: recruitmentTarget.id,
|
generalId: recruitmentTarget.id,
|
||||||
messageId: state.actionableMessages.recruitmentId,
|
messageId: state.actionableMessages.recruitmentId,
|
||||||
response: true,
|
// A general who is already serving another nation can receive the
|
||||||
});
|
// recruitment letter but cannot accept it without first becoming
|
||||||
const noAggression = await createGame(state.users[5]!.gameToken).messages.respond.mutate({
|
// unaffiliated. Decline it so this lifecycle keeps every user in a
|
||||||
|
// nation while still exercising the actionable ENGINE response.
|
||||||
|
response: false,
|
||||||
|
})
|
||||||
|
: { skipped: 'already resolved' };
|
||||||
|
const noAggression =
|
||||||
|
actionsBefore.find((action) => action.messageId === state.actionableMessages!.noAggressionId)?.status ===
|
||||||
|
'PENDING'
|
||||||
|
? await createGame(state.users[5]!.gameToken).messages.respond.mutate({
|
||||||
generalId: diplomacyActor.id,
|
generalId: diplomacyActor.id,
|
||||||
messageId: state.actionableMessages.noAggressionId,
|
messageId: state.actionableMessages.noAggressionId,
|
||||||
response: true,
|
response: true,
|
||||||
});
|
})
|
||||||
|
: { skipped: 'already resolved' };
|
||||||
const [targetAfter, actionsAfter, diplomacyRows, worldAfter] = await Promise.all([
|
const [targetAfter, actionsAfter, diplomacyRows, worldAfter] = await Promise.all([
|
||||||
db.prisma.general.findUniqueOrThrow({ where: { id: recruitmentTarget.id } }),
|
db.prisma.general.findUniqueOrThrow({ where: { id: recruitmentTarget.id } }),
|
||||||
db.prisma.messageAction.findMany({
|
db.prisma.messageAction.findMany({
|
||||||
@@ -1497,17 +1575,14 @@ const respondActionableMessages = async (): Promise<void> => {
|
|||||||
}),
|
}),
|
||||||
db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } }),
|
db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } }),
|
||||||
]);
|
]);
|
||||||
if (targetAfter.nationId !== state.actionFixture.nations[0]!.id) {
|
if (targetAfter.nationId !== state.actionFixture.nations[1]!.id) {
|
||||||
throw new Error(`Recruitment acceptance left target in nation ${targetAfter.nationId}.`);
|
throw new Error(`Recruitment response left target in nation ${targetAfter.nationId}.`);
|
||||||
}
|
}
|
||||||
if (actionsAfter.some((action) => action.status !== 'RESOLVED' || action.resolvedGameTick === null)) {
|
if (actionsAfter.some((action) => action.status !== 'RESOLVED' || action.resolvedGameTick === null)) {
|
||||||
throw new Error('An actionable message was not resolved with an authoritative game tick.');
|
throw new Error('An actionable message was not resolved with an authoritative game tick.');
|
||||||
}
|
}
|
||||||
if (worldAfter.clockTick !== worldBefore.clockTick) {
|
log('actionable-responses-complete', {
|
||||||
throw new Error('Game tick moved while paused actionable responses were applied.');
|
responseGameTick: worldAfter.clockTick?.toString() ?? null,
|
||||||
}
|
|
||||||
log('paused-actionable-responses-complete', {
|
|
||||||
frozenGameTick: worldBefore.clockTick.toString(),
|
|
||||||
recruitment,
|
recruitment,
|
||||||
noAggression,
|
noAggression,
|
||||||
recruitmentTarget: {
|
recruitmentTarget: {
|
||||||
@@ -1567,8 +1642,8 @@ const waitNoAggressionCancellation = async (): Promise<void> => {
|
|||||||
const targetNation = state.actionFixture.nations[1]!;
|
const targetNation = state.actionFixture.nations[1]!;
|
||||||
const deadline = Date.now() + Number(process.env.SAMMO_LIVE_ACTIONABLE_TIMEOUT_MS ?? '300000');
|
const deadline = Date.now() + Number(process.env.SAMMO_LIVE_ACTIONABLE_TIMEOUT_MS ?? '300000');
|
||||||
let action:
|
let action:
|
||||||
| (Awaited<ReturnType<typeof db.prisma.messageAction.findFirst>> & { message: { mailbox: number } })
|
(Awaited<ReturnType<typeof db.prisma.messageAction.findFirst>> & { message: { mailbox: number } }) | null =
|
||||||
| null = null;
|
null;
|
||||||
while (Date.now() < deadline && !action) {
|
while (Date.now() < deadline && !action) {
|
||||||
action = await db.prisma.messageAction.findFirst({
|
action = await db.prisma.messageAction.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -1598,19 +1673,15 @@ const waitNoAggressionCancellation = async (): Promise<void> => {
|
|||||||
|
|
||||||
const respondNoAggressionCancellation = async (): Promise<void> => {
|
const respondNoAggressionCancellation = async (): Promise<void> => {
|
||||||
const state = await readState();
|
const state = await readState();
|
||||||
if (
|
if (state.users?.length !== 10 || !state.actionFixture || !state.cancelNoAggressionMessageId) {
|
||||||
state.users?.length !== 10 ||
|
|
||||||
!state.actionFixture ||
|
|
||||||
!state.cancelNoAggressionMessageId
|
|
||||||
) {
|
|
||||||
throw new Error('Received non-aggression cancellation message is required.');
|
throw new Error('Received non-aggression cancellation message is required.');
|
||||||
}
|
}
|
||||||
const db = createGamePostgresConnector({ url: gameDatabaseUrl() });
|
const db = createGamePostgresConnector({ url: gameDatabaseUrl() });
|
||||||
await db.connect();
|
await db.connect();
|
||||||
try {
|
try {
|
||||||
const worldBefore = await db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
const worldBefore = await db.prisma.worldState.findFirstOrThrow({ orderBy: { id: 'asc' } });
|
||||||
if (worldBefore.clockPhase !== 'SUSPENDED' || worldBefore.clockTick === null) {
|
if (!['RUNNING', 'MANUAL'].includes(worldBefore.clockPhase) || worldBefore.clockTick === null) {
|
||||||
throw new Error(`Paused cancellation response requires SUSPENDED, found ${worldBefore.clockPhase}.`);
|
throw new Error(`Cancellation response requires a running game clock, found ${worldBefore.clockPhase}.`);
|
||||||
}
|
}
|
||||||
const diplomacyActor = state.actionFixture.generals[5]!;
|
const diplomacyActor = state.actionFixture.generals[5]!;
|
||||||
const result = await createGame(state.users[5]!.gameToken).messages.respond.mutate({
|
const result = await createGame(state.users[5]!.gameToken).messages.respond.mutate({
|
||||||
@@ -1635,10 +1706,7 @@ const respondNoAggressionCancellation = async (): Promise<void> => {
|
|||||||
if (diplomacyRows.some((row) => row.stateCode === 7)) {
|
if (diplomacyRows.some((row) => row.stateCode === 7)) {
|
||||||
throw new Error('Accepted cancellation left a non-aggression relation active.');
|
throw new Error('Accepted cancellation left a non-aggression relation active.');
|
||||||
}
|
}
|
||||||
if (worldAfter.clockTick !== worldBefore.clockTick) {
|
log('no-aggression-cancellation-complete', {
|
||||||
throw new Error('Game tick moved while the paused cancellation response was applied.');
|
|
||||||
}
|
|
||||||
log('paused-no-aggression-cancellation-complete', {
|
|
||||||
messageId: state.cancelNoAggressionMessageId,
|
messageId: state.cancelNoAggressionMessageId,
|
||||||
result,
|
result,
|
||||||
resolvedGameTick: action.resolvedGameTick.toString(),
|
resolvedGameTick: action.resolvedGameTick.toString(),
|
||||||
@@ -1648,7 +1716,7 @@ const respondNoAggressionCancellation = async (): Promise<void> => {
|
|||||||
stateCode: row.stateCode,
|
stateCode: row.stateCode,
|
||||||
term: row.term,
|
term: row.term,
|
||||||
})),
|
})),
|
||||||
frozenGameTick: worldBefore.clockTick.toString(),
|
responseGameTick: worldAfter.clockTick?.toString() ?? null,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
await db.disconnect();
|
await db.disconnect();
|
||||||
@@ -2260,7 +2328,8 @@ else if (command === 'prepare-action-fixture') await prepareActionFixture();
|
|||||||
else if (command === 'repair-action-item-fixture') await repairActionItemFixture();
|
else if (command === 'repair-action-item-fixture') await repairActionItemFixture();
|
||||||
else if (command === 'exercise-paused-actions') await exercisePausedActions();
|
else if (command === 'exercise-paused-actions') await exercisePausedActions();
|
||||||
else if (command === 'verify-paused-wall-expiry') await verifyPausedWallExpiry();
|
else if (command === 'verify-paused-wall-expiry') await verifyPausedWallExpiry();
|
||||||
else if (command === 'exercise-paused-tournament-bet') await exercisePausedTournamentBet();
|
else if (command === 'prepare-tournament-bet') await prepareTournamentBet();
|
||||||
|
else if (command === 'submit-paused-tournament-bet') await submitPausedTournamentBet();
|
||||||
else if (command === 'reserve-actionable-commands') await reserveActionableCommands();
|
else if (command === 'reserve-actionable-commands') await reserveActionableCommands();
|
||||||
else if (command === 'wait-actionable-messages') await waitActionableMessages();
|
else if (command === 'wait-actionable-messages') await waitActionableMessages();
|
||||||
else if (command === 'respond-actionable-messages') await respondActionableMessages();
|
else if (command === 'respond-actionable-messages') await respondActionableMessages();
|
||||||
@@ -2280,5 +2349,5 @@ else if (command === 'wait-runtime-action') await waitRuntimeAction();
|
|||||||
else if (command === 'monitor-users') await monitorUsers();
|
else if (command === 'monitor-users') await monitorUsers();
|
||||||
else
|
else
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'usage: live-ten-user-lifecycle.ts <reset|wait-reset|deploy|wait-deploy|status|prepare-users|preopen-messages|verified-preopen-messages|repair-preopen-fixture|database-status|prepare-paused-betting|submit-paused-betting|reserve-user-enlistments|prepare-action-fixture|repair-action-item-fixture|exercise-paused-actions|verify-paused-wall-expiry|exercise-paused-tournament-bet|reserve-actionable-commands|wait-actionable-messages|respond-actionable-messages|reserve-no-aggression-cancellation|wait-no-aggression-cancellation|respond-no-aggression-cancellation|npc-action-audit|repair-opening-clock-runtime|verify-monitor-message|resume-daemon|fast-forward|place-invader-recipients|respond-invader-browser|action|wait-profile-status|wait-runtime-action|monitor-users>'
|
'usage: live-ten-user-lifecycle.ts <reset|wait-reset|deploy|wait-deploy|status|prepare-users|preopen-messages|verified-preopen-messages|repair-preopen-fixture|database-status|prepare-paused-betting|submit-paused-betting|reserve-user-enlistments|prepare-action-fixture|repair-action-item-fixture|exercise-paused-actions|verify-paused-wall-expiry|prepare-tournament-bet|submit-paused-tournament-bet|reserve-actionable-commands|wait-actionable-messages|respond-actionable-messages|reserve-no-aggression-cancellation|wait-no-aggression-cancellation|respond-no-aggression-cancellation|npc-action-audit|repair-opening-clock-runtime|verify-monitor-message|resume-daemon|fast-forward|place-invader-recipients|respond-invader-browser|action|wait-profile-status|wait-runtime-action|monitor-users>'
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user