Merge origin/main into frontend parity worktree
This commit is contained in:
@@ -8,19 +8,19 @@ export const composeCalendarHandlers = (
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
beforeMonthChanged: (context) => {
|
||||
beforeMonthChanged: async (context) => {
|
||||
for (const handler of resolved) {
|
||||
handler.beforeMonthChanged?.(context);
|
||||
await handler.beforeMonthChanged?.(context);
|
||||
}
|
||||
},
|
||||
onMonthChanged: (context) => {
|
||||
onMonthChanged: async (context) => {
|
||||
for (const handler of resolved) {
|
||||
handler.onMonthChanged?.(context);
|
||||
await handler.onMonthChanged?.(context);
|
||||
}
|
||||
},
|
||||
onYearChanged: (context) => {
|
||||
onYearChanged: async (context) => {
|
||||
for (const handler of resolved) {
|
||||
handler.onYearChanged?.(context);
|
||||
await handler.onYearChanged?.(context);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -36,6 +36,17 @@ const zAuctionFinalize = z.object({
|
||||
auctionId: zFiniteNumber,
|
||||
});
|
||||
|
||||
const zAuctionOpen = z.object({
|
||||
type: z.literal('auctionOpen'),
|
||||
generalId: zFiniteNumber,
|
||||
auctionType: z.enum(['BUY_RICE', 'SELL_RICE', 'UNIQUE_ITEM']),
|
||||
amount: zFiniteNumber,
|
||||
closeTurnCnt: zFiniteNumber.optional(),
|
||||
startBidAmount: zFiniteNumber.optional(),
|
||||
finishBidAmount: zFiniteNumber.optional(),
|
||||
itemKey: z.string().optional(),
|
||||
});
|
||||
|
||||
const zAuctionBid = z.object({
|
||||
type: z.literal('auctionBid'),
|
||||
auctionId: zFiniteNumber,
|
||||
@@ -266,6 +277,14 @@ const normalizeAuctionFinalize: CommandNormalizer<'auctionFinalize'> = (envelope
|
||||
return { ...command, requestId: envelope.requestId };
|
||||
};
|
||||
|
||||
const normalizeAuctionOpen: CommandNormalizer<'auctionOpen'> = (envelope) => {
|
||||
const command = parseWith(zAuctionOpen, envelope.command);
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
return { ...command, requestId: envelope.requestId };
|
||||
};
|
||||
|
||||
const normalizeAuctionBid: CommandNormalizer<'auctionBid'> = (envelope) => {
|
||||
const command = parseWith(zAuctionBid, envelope.command);
|
||||
if (!command) {
|
||||
@@ -488,6 +507,7 @@ const normalizeShutdown: CommandNormalizer<'shutdown'> = (envelope) => {
|
||||
|
||||
const normalizers: CommandNormalizerMap = {
|
||||
auctionFinalize: normalizeAuctionFinalize,
|
||||
auctionOpen: normalizeAuctionOpen,
|
||||
auctionBid: normalizeAuctionBid,
|
||||
troopCreate: normalizeTroopCreate,
|
||||
troopJoin: normalizeTroopJoin,
|
||||
|
||||
@@ -344,6 +344,7 @@ export const createDatabaseTurnHooks = async (
|
||||
createdDiplomacy,
|
||||
deletedEvents,
|
||||
lifecycleEvents,
|
||||
pendingNeutralAuctions,
|
||||
} = changes;
|
||||
const reservedTurnChanges = options?.reservedTurns?.peekDirtyState();
|
||||
|
||||
@@ -354,6 +355,28 @@ export const createDatabaseTurnHooks = async (
|
||||
meta: asJson(state.meta),
|
||||
};
|
||||
const persist = async (prisma: GamePrisma.TransactionClient): Promise<void> => {
|
||||
let neutralAuctionsToCreate = pendingNeutralAuctions;
|
||||
if (pendingNeutralAuctions.length > 0) {
|
||||
const latestRegistrationKey =
|
||||
pendingNeutralAuctions[pendingNeutralAuctions.length - 1]!.registrationKey;
|
||||
await prisma.$executeRaw`
|
||||
SELECT pg_advisory_xact_lock(
|
||||
hashtext(${'neutral-auction-registration'}),
|
||||
${state.id}
|
||||
)
|
||||
`;
|
||||
const persistedRows = await prisma.$queryRaw<Array<{ meta: unknown }>>`
|
||||
SELECT meta
|
||||
FROM world_state
|
||||
WHERE id = ${state.id}
|
||||
FOR UPDATE
|
||||
`;
|
||||
const persistedMeta = asRecord(persistedRows[0]?.meta);
|
||||
if (persistedMeta.neutralAuctionRegistrationKey === latestRegistrationKey) {
|
||||
neutralAuctionsToCreate = [];
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.worldState.update({
|
||||
where: { id: state.id },
|
||||
data: worldStateUpdate,
|
||||
@@ -436,6 +459,20 @@ export const createDatabaseTurnHooks = async (
|
||||
);
|
||||
}
|
||||
|
||||
if (neutralAuctionsToCreate.length > 0) {
|
||||
await prisma.auction.createMany({
|
||||
data: neutralAuctionsToCreate.map((auction) => ({
|
||||
type: auction.type,
|
||||
targetCode: auction.targetCode,
|
||||
hostGeneralId: auction.hostGeneralId,
|
||||
hostName: auction.hostName,
|
||||
detail: asJson(auction.detail),
|
||||
status: 'OPEN',
|
||||
closeAt: auction.closeAt,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
const createdIds = new Set(createdGenerals.map((general) => general.id));
|
||||
const createdNationIds = new Set(createdNations.map((nation) => nation.id));
|
||||
const createdTroopIds = new Set(createdTroops.map((troop) => troop.id));
|
||||
|
||||
@@ -96,7 +96,7 @@ export class InMemoryTurnProcessor implements TurnProcessor {
|
||||
partial = true;
|
||||
break;
|
||||
}
|
||||
this.world.advanceMonth(nextTickTime);
|
||||
await this.world.advanceMonth(nextTickTime);
|
||||
processedTurns += 1;
|
||||
nextTickTime = getNextTickTime(this.world.getState().lastTurnTime, this.tickMinutes);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,14 @@ import type { City, LogEntryDraft, MessageDraft, Nation, ScenarioConfig, Troop,
|
||||
import { getNextTurnAt } from '@sammo-ts/logic';
|
||||
|
||||
import type { TurnCheckpoint } from '../lifecycle/types.js';
|
||||
import type { TurnDiplomacy, TurnEvent, TurnGeneral, TurnWorldSnapshot, TurnWorldState } from './types.js';
|
||||
import type {
|
||||
PendingNeutralAuction,
|
||||
TurnDiplomacy,
|
||||
TurnEvent,
|
||||
TurnGeneral,
|
||||
TurnWorldSnapshot,
|
||||
TurnWorldState,
|
||||
} from './types.js';
|
||||
import {
|
||||
applyDiplomacyPatch as applyDiplomacyPatchToEntry,
|
||||
buildDefaultDiplomacy,
|
||||
@@ -73,9 +80,9 @@ export interface TurnCalendarContext {
|
||||
|
||||
export interface TurnCalendarHandler {
|
||||
// 레거시 PRE_MONTH는 날짜 변경 전, MONTH는 날짜 변경 후에 실행된다.
|
||||
beforeMonthChanged?(context: TurnCalendarContext): void;
|
||||
onMonthChanged?(context: TurnCalendarContext): void;
|
||||
onYearChanged?(context: TurnCalendarContext): void;
|
||||
beforeMonthChanged?(context: TurnCalendarContext): void | Promise<void>;
|
||||
onMonthChanged?(context: TurnCalendarContext): void | Promise<void>;
|
||||
onYearChanged?(context: TurnCalendarContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface InMemoryTurnWorldOptions {
|
||||
@@ -102,6 +109,7 @@ export interface TurnWorldChanges {
|
||||
createdDiplomacy: TurnDiplomacy[];
|
||||
deletedEvents: number[];
|
||||
lifecycleEvents: GeneralLifecycleEvent[];
|
||||
pendingNeutralAuctions: PendingNeutralAuction[];
|
||||
}
|
||||
|
||||
const compareTurnOrder = (left: TurnGeneral, right: TurnGeneral): number => {
|
||||
@@ -270,6 +278,7 @@ export class InMemoryTurnWorld {
|
||||
private readonly logs: LogEntryDraft[] = [];
|
||||
private readonly messages: MessageDraft[] = [];
|
||||
private readonly lifecycleEvents: GeneralLifecycleEvent[] = [];
|
||||
private readonly pendingNeutralAuctions: PendingNeutralAuction[] = [];
|
||||
private readonly scenarioConfig: ScenarioConfig;
|
||||
private checkpoint?: TurnCheckpoint;
|
||||
private state: TurnWorldState;
|
||||
@@ -331,6 +340,14 @@ export class InMemoryTurnWorld {
|
||||
this.logs.push(entry);
|
||||
}
|
||||
|
||||
queueNeutralAuction(auction: PendingNeutralAuction): void {
|
||||
this.pendingNeutralAuctions.push({
|
||||
...auction,
|
||||
detail: { ...auction.detail },
|
||||
closeAt: new Date(auction.closeAt.getTime()),
|
||||
});
|
||||
}
|
||||
|
||||
getScenarioConfig(): ScenarioConfig {
|
||||
return this.scenarioConfig;
|
||||
}
|
||||
@@ -744,7 +761,7 @@ export class InMemoryTurnWorld {
|
||||
return nextTurnAt;
|
||||
}
|
||||
|
||||
advanceMonth(turnTime: Date): void {
|
||||
async advanceMonth(turnTime: Date): Promise<void> {
|
||||
const previousYear = this.state.currentYear;
|
||||
const previousMonth = this.state.currentMonth;
|
||||
let nextYear = previousYear;
|
||||
@@ -761,7 +778,7 @@ export class InMemoryTurnWorld {
|
||||
currentMonth: nextMonth,
|
||||
turnTime,
|
||||
};
|
||||
this.calendarHandler?.beforeMonthChanged?.(context);
|
||||
await this.calendarHandler?.beforeMonthChanged?.(context);
|
||||
|
||||
const meta = {
|
||||
...this.state.meta,
|
||||
@@ -776,9 +793,9 @@ export class InMemoryTurnWorld {
|
||||
};
|
||||
|
||||
this.advanceDiplomacyMonth();
|
||||
this.calendarHandler?.onMonthChanged?.(context);
|
||||
await this.calendarHandler?.onMonthChanged?.(context);
|
||||
if (nextYear !== previousYear) {
|
||||
this.calendarHandler?.onYearChanged?.(context);
|
||||
await this.calendarHandler?.onYearChanged?.(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,6 +835,11 @@ export class InMemoryTurnWorld {
|
||||
const logs = this.logs.slice();
|
||||
const messages = this.messages.slice();
|
||||
const lifecycleEvents = this.lifecycleEvents.slice();
|
||||
const pendingNeutralAuctions = this.pendingNeutralAuctions.map((auction) => ({
|
||||
...auction,
|
||||
detail: { ...auction.detail },
|
||||
closeAt: new Date(auction.closeAt.getTime()),
|
||||
}));
|
||||
|
||||
return {
|
||||
generals,
|
||||
@@ -837,6 +859,7 @@ export class InMemoryTurnWorld {
|
||||
createdDiplomacy,
|
||||
deletedEvents,
|
||||
lifecycleEvents,
|
||||
pendingNeutralAuctions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -862,6 +885,7 @@ export class InMemoryTurnWorld {
|
||||
this.logs.splice(0, changes.logs.length);
|
||||
this.messages.splice(0, changes.messages.length);
|
||||
this.lifecycleEvents.splice(0, changes.lifecycleEvents.length);
|
||||
this.pendingNeutralAuctions.splice(0, changes.pendingNeutralAuctions.length);
|
||||
}
|
||||
|
||||
consumeDirtyState(): TurnWorldChanges {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { shouldUseAi } from './ai/generalAi.js';
|
||||
import { createUnificationHandler } from './unificationHandler.js';
|
||||
import { createAuctionFinalizer } from '../auction/finalizer.js';
|
||||
import { createAuctionBidder } from '../auction/bidder.js';
|
||||
import { createNeutralAuctionRegistrar } from '../auction/neutralRegistrar.js';
|
||||
import { createTournamentRewardFinalizer } from '../tournament/finalizer.js';
|
||||
import { createTournamentAutoStartHandler } from './tournamentAutoStart.js';
|
||||
import { createYearbookHandler } from './yearbookHandler.js';
|
||||
@@ -198,6 +199,13 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
getWorld: () => worldRef,
|
||||
map: snapshot.map ?? null,
|
||||
});
|
||||
const neutralAuctionRegistrar = await createNeutralAuctionRegistrar({
|
||||
databaseUrl: options.databaseUrl,
|
||||
profileName: options.profileName ?? options.profile,
|
||||
getWorld: () => worldRef,
|
||||
getRedisClient: () => redisConnector?.client,
|
||||
getWorldConfig: () => snapshot.worldConfig ?? null,
|
||||
});
|
||||
const tournamentAutoStartHandler = createTournamentAutoStartHandler({
|
||||
profileName: options.profileName ?? options.profile,
|
||||
getRedisClient: () => redisConnector?.client,
|
||||
@@ -215,6 +223,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
nationTurnMonthlyHandler,
|
||||
hasEventAction('ProcessIncome') ? null : incomeHandler,
|
||||
frontStateHandler,
|
||||
neutralAuctionRegistrar.handler,
|
||||
tournamentAutoStartHandler,
|
||||
yearbookHandler.handler
|
||||
);
|
||||
@@ -394,6 +403,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
const baseClose = close;
|
||||
close = async () => {
|
||||
await baseClose();
|
||||
await neutralAuctionRegistrar.close();
|
||||
if (unification) {
|
||||
await unification.close();
|
||||
}
|
||||
|
||||
@@ -49,6 +49,16 @@ export interface TurnEvent {
|
||||
meta: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PendingNeutralAuction {
|
||||
registrationKey: string;
|
||||
type: 'BUY_RICE' | 'SELL_RICE';
|
||||
targetCode: string;
|
||||
hostGeneralId: 0;
|
||||
hostName: '상인';
|
||||
detail: Record<string, unknown>;
|
||||
closeAt: Date;
|
||||
}
|
||||
|
||||
export interface TurnWorldSnapshot extends Omit<
|
||||
WorldSnapshot,
|
||||
'generals' | 'cities' | 'nations' | 'troops' | 'diplomacy'
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '@sammo-ts/logic/items/index.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { TurnGeneral } from './types.js';
|
||||
import { openAuction } from '../auction/opener.js';
|
||||
|
||||
let itemRegistryPromise: Promise<Map<string, ItemModule>> | null = null;
|
||||
|
||||
@@ -830,6 +831,13 @@ async function handleAuctionFinalize(
|
||||
return ctx.auctionFinalizer.finalize(command.auctionId, ctx.commandDb);
|
||||
}
|
||||
|
||||
async function handleAuctionOpen(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'auctionOpen' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
return openAuction(command, ctx.world, ctx.commandDb);
|
||||
}
|
||||
|
||||
async function handleAuctionBid(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'auctionBid' }>
|
||||
@@ -1340,6 +1348,8 @@ export const createTurnDaemonCommandHandler = (options: {
|
||||
dropItem: (command) => handleDropItem(ctx, command as Extract<TurnDaemonCommand, { type: 'dropItem' }>),
|
||||
auctionFinalize: (command) =>
|
||||
handleAuctionFinalize(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionFinalize' }>),
|
||||
auctionOpen: (command) =>
|
||||
handleAuctionOpen(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionOpen' }>),
|
||||
auctionBid: (command) => handleAuctionBid(ctx, command as Extract<TurnDaemonCommand, { type: 'auctionBid' }>),
|
||||
changePermission: (command) =>
|
||||
handleChangePermission(ctx, command as Extract<TurnDaemonCommand, { type: 'changePermission' }>),
|
||||
|
||||
Reference in New Issue
Block a user