fix: 가오픈 명령과 GAME WALL 시간 경계 회귀 수정
This commit is contained in:
@@ -1711,7 +1711,11 @@ export class InMemoryTurnWorld {
|
||||
// Rebasing is also the explicit resume checkpoint. Realtime mode
|
||||
// must not replay the operational downtime after an administrator
|
||||
// deliberately delays or accelerates the game schedule.
|
||||
clockWallAnchor: new Date(wallNow.getTime()),
|
||||
// 가오픈의 anchor는 별도 예약된 정식 오픈이다. 표시 좌표를 옮기는
|
||||
// 작업이 그 미래 경계를 현재 시각으로 당겨 게임을 시작시키면 안 된다.
|
||||
clockWallAnchor: new Date(
|
||||
previousClock.phase === 'PREOPEN' ? previousClock.wallAnchor.getTime() : wallNow.getTime()
|
||||
),
|
||||
lastTurnTime: nextLastTurnTime,
|
||||
meta: nextMeta,
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ export const normalizeJoinSpecialityCode = (value: unknown): string | null =>
|
||||
export const resolveLegacyPenalty = (
|
||||
rawPenalty: Record<string, unknown> | undefined,
|
||||
profileId: string,
|
||||
acceptedAt: Date
|
||||
requestedAtWall: Date
|
||||
): Record<string, unknown> => {
|
||||
if (!rawPenalty) {
|
||||
return {};
|
||||
@@ -105,7 +105,7 @@ export const resolveLegacyPenalty = (
|
||||
...asRecord(rawPenalty.any),
|
||||
...asRecord(rawPenalty[profileId]),
|
||||
};
|
||||
const acceptedAtSeconds = acceptedAt.getTime() / 1000;
|
||||
const acceptedAtSeconds = requestedAtWall.getTime() / 1000;
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, rawEntry] of Object.entries(merged)) {
|
||||
const entry = asRecord(rawEntry);
|
||||
@@ -654,12 +654,7 @@ export const createGeneralFromJoin = async (options: {
|
||||
const generalId = world.getNextGeneralId();
|
||||
const rng = new RandUtil(
|
||||
new LiteHashDRBG(
|
||||
buildJoinCreateGeneralSeed(
|
||||
hiddenSeed,
|
||||
input.seedOwnerIdentity,
|
||||
world.dateToGameTick(acceptedAt),
|
||||
generalId
|
||||
)
|
||||
buildJoinCreateGeneralSeed(hiddenSeed, input.seedOwnerIdentity, world.dateToGameTick(acceptedAt), generalId)
|
||||
)
|
||||
);
|
||||
const geniusRequested = input.inheritSpecial !== undefined || rng.nextBool(0.01);
|
||||
@@ -805,7 +800,9 @@ export const createGeneralFromJoin = async (options: {
|
||||
meta: {},
|
||||
},
|
||||
lastTurn: { command: DEFAULT_TURN_ACTION },
|
||||
penalty: resolveLegacyPenalty(input.ownerLegacyPenalty, input.profileId, acceptedAt),
|
||||
// 계정 제재 expire는 Ref member의 Unix wall timestamp다. 가오픈의
|
||||
// 음수 GAME 좌표로 비교하면 현실에서 만료된 제재가 다시 적용된다.
|
||||
penalty: resolveLegacyPenalty(input.ownerLegacyPenalty, input.profileId, operationalAcceptedAt),
|
||||
inheritancePoints: {
|
||||
previous: finalInheritancePoint,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||
import type { TurnWorldState } from './types.js';
|
||||
|
||||
const DEFAULT_MIN_TURNS = 2;
|
||||
|
||||
/** 표시용 opentime과 재투영된 lastTurnTime 대신 오픈 phase와 실행 cursor로 판정한다. */
|
||||
export const hasStartedForPrestartActions = (
|
||||
state: Pick<TurnWorldState, 'clockPhase' | 'lastTurnTick' | 'lastTurnTime' | 'meta'>
|
||||
): boolean => {
|
||||
if (state.clockPhase === 'PREOPEN') return false;
|
||||
if (state.clockPhase === 'COMPLETED' || state.clockPhase === 'SUSPENDED' || state.clockPhase === 'RECONCILING') {
|
||||
return true;
|
||||
}
|
||||
// Ref의 turntime == opentime 경계는 아직 허용하고 최초 월 진행 이후 닫는다.
|
||||
if (state.lastTurnTick !== undefined) return state.lastTurnTick > 0;
|
||||
const opentime = typeof state.meta.opentime === 'string' ? new Date(state.meta.opentime) : null;
|
||||
return opentime !== null && state.lastTurnTime.getTime() > opentime.getTime();
|
||||
};
|
||||
|
||||
export const readPrestartDeleteAfter = (meta: Record<string, unknown>): Date | null => {
|
||||
const value = meta.prestart_delete_after;
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
|
||||
@@ -70,7 +70,12 @@ import {
|
||||
} from './selectPoolService.js';
|
||||
import { createGeneralFromJoin, JoinCreateGeneralError } from './joinCreateGeneralService.js';
|
||||
import { NpcPossessionError, possessNpcGeneral } from './npcPossessionService.js';
|
||||
import { buildPrestartDeleteAfter, formatPrestartDeleteAfter, readPrestartDeleteAfter } from './prestartDeletion.js';
|
||||
import {
|
||||
buildPrestartDeleteAfter,
|
||||
formatPrestartDeleteAfter,
|
||||
readPrestartDeleteAfter,
|
||||
hasStartedForPrestartActions,
|
||||
} from './prestartDeletion.js';
|
||||
import { respondToActionableMessage } from './actionableMessageResponse.js';
|
||||
import { executeInheritanceAction } from './inheritanceActionService.js';
|
||||
import { applyNpcPolicyMutation } from './npcPolicyMutation.js';
|
||||
@@ -344,8 +349,14 @@ async function handleJoinCreateGeneral(
|
||||
throw new Error('Join world state is missing.');
|
||||
}
|
||||
const operationalAcceptedAt = await resolveCommandAcceptedAt(db, command);
|
||||
const acceptedAt = ctx.world.getGameNow(operationalAcceptedAt);
|
||||
const turnScheduleAt = ctx.world.getRunnableGameNow(operationalAcceptedAt);
|
||||
const processingGameTick = Reflect.get(command, 'processingGameTick');
|
||||
if (typeof processingGameTick !== 'number' || !Number.isSafeInteger(processingGameTick)) {
|
||||
throw new Error('joinCreateGeneral requires an authoritative daemon processing game tick.');
|
||||
}
|
||||
const acceptedAt = ctx.world.gameTickToDate(processingGameTick);
|
||||
const turnScheduleAt = ctx.world.gameTickToDate(
|
||||
ctx.world.getGameClockState().phase === 'PREOPEN' ? Math.max(0, processingGameTick) : processingGameTick
|
||||
);
|
||||
try {
|
||||
return {
|
||||
type: 'joinCreateGeneral',
|
||||
@@ -463,7 +474,10 @@ async function handleSelectPoolCreate(
|
||||
throw new Error('selectPoolCreate requires an authoritative daemon processing game tick.');
|
||||
}
|
||||
const acceptedAt = ctx.world.gameTickToDate(processingGameTick);
|
||||
const turnScheduleAt = acceptedAt;
|
||||
// 선택 생성도 접수/RNG의 음수 tick과 실제 최초 턴의 오픈 하한을 분리한다.
|
||||
const turnScheduleAt = ctx.world.gameTickToDate(
|
||||
ctx.world.getGameClockState().phase === 'PREOPEN' ? Math.max(0, processingGameTick) : processingGameTick
|
||||
);
|
||||
try {
|
||||
return {
|
||||
type: 'selectPoolCreate',
|
||||
@@ -1607,8 +1621,7 @@ async function handleEnsureDieOnPrestartStatus(
|
||||
const db = requireCommandDatabase(ctx);
|
||||
const acceptedAt = await resolveCommandAcceptedAt(db, command);
|
||||
const worldState = ctx.world.getState();
|
||||
const opentime = typeof worldState.meta.opentime === 'string' ? worldState.meta.opentime : null;
|
||||
if ((opentime && worldState.lastTurnTime.getTime() > new Date(opentime).getTime()) || general.nationId !== 0) {
|
||||
if (hasStartedForPrestartActions(worldState) || general.nationId !== 0) {
|
||||
return {
|
||||
type: 'ensureDieOnPrestartStatus',
|
||||
generalId: command.generalId,
|
||||
@@ -1643,8 +1656,7 @@ async function handleDieOnPrestart(
|
||||
}
|
||||
const acceptedAt = await resolveCommandAcceptedAt(db, command);
|
||||
const worldState = world.getState();
|
||||
const opentime = worldState.meta.opentime as string | undefined;
|
||||
if (opentime && new Date(worldState.lastTurnTime) > new Date(opentime)) {
|
||||
if (hasStartedForPrestartActions(worldState)) {
|
||||
return {
|
||||
type: 'dieOnPrestart',
|
||||
ok: false,
|
||||
@@ -1709,8 +1721,7 @@ async function handleBuildNationCandidate(
|
||||
await assertImmediateGeneralActionActor(ctx, command, general);
|
||||
|
||||
const worldState = world.getState();
|
||||
const opentime = worldState.meta.opentime as string | undefined;
|
||||
if (opentime && new Date(worldState.lastTurnTime) > new Date(opentime)) {
|
||||
if (hasStartedForPrestartActions(worldState)) {
|
||||
return {
|
||||
type: 'buildNationCandidate',
|
||||
ok: false,
|
||||
|
||||
Reference in New Issue
Block a user