feat: 정지·종료 기수의 장수 참여를 허용하고 확정 기록 보호
This commit is contained in:
@@ -169,6 +169,13 @@ export class DatabaseTurnDaemonCommandQueue implements TurnDaemonControlQueue, T
|
||||
AND (
|
||||
${gameplayAllowed}
|
||||
OR "event_type" = 'getStatus'
|
||||
OR (
|
||||
${world?.clockPhase === 'SUSPENDED' || world?.clockPhase === 'COMPLETED'}
|
||||
AND "event_type" IN (
|
||||
'joinCreateGeneral', 'npcPossessGeneral',
|
||||
'selectPoolReserve', 'selectPoolCreate', 'selectPoolReselect'
|
||||
)
|
||||
)
|
||||
OR (
|
||||
${suspendedTournamentBetCommand}
|
||||
AND "event_type" IN ('adjustGeneralResources', 'adjustGeneralMeta')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { areSeasonRecordsFinalized } from '../turn/seasonRecords.js';
|
||||
import { JosaUtil, asRecord } from '@sammo-ts/common';
|
||||
import { createGamePostgresConnector, type GamePrisma } from '@sammo-ts/infra';
|
||||
import { ActionLogger, LogFormat, type TournamentType, type TriggerValue } from '@sammo-ts/logic';
|
||||
@@ -253,7 +254,8 @@ export const createTournamentRewardFinalizer = async (options: {
|
||||
}))
|
||||
.filter((entry) => !!entry.userId);
|
||||
|
||||
for (const entry of pointUpdates) {
|
||||
const recordsFinalized = await areSeasonRecordsFinalized(db, world.getState().meta.serverId);
|
||||
for (const entry of recordsFinalized ? [] : pointUpdates) {
|
||||
await db.inheritancePoint.upsert({
|
||||
where: {
|
||||
userId_key: { userId: entry.userId!, key: 'tournament' },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { areSeasonRecordsFinalized } from './seasonRecords.js';
|
||||
import {
|
||||
acquireGameSchemaAdvisoryXactLock,
|
||||
CLOCK_OPERATION_PERSISTENCE_LOCK,
|
||||
@@ -602,7 +603,8 @@ const persistNationBettingOpen = async (
|
||||
|
||||
const persistNationBettingFinish = async (
|
||||
prisma: GamePrisma.TransactionClient,
|
||||
finish: PendingNationBettingFinish
|
||||
finish: PendingNationBettingFinish,
|
||||
recordsFinalized: boolean
|
||||
): Promise<void> => {
|
||||
await prisma.$queryRaw`
|
||||
SELECT id
|
||||
@@ -652,7 +654,7 @@ const persistNationBettingFinish = async (
|
||||
})),
|
||||
});
|
||||
|
||||
for (const reward of rewards) {
|
||||
for (const reward of recordsFinalized ? [] : rewards) {
|
||||
if (!reward.userId) {
|
||||
continue;
|
||||
}
|
||||
@@ -1451,11 +1453,12 @@ export const createDatabaseTurnHooks = async (
|
||||
`);
|
||||
}
|
||||
|
||||
const recordsFinalized = await areSeasonRecordsFinalized(prisma, asRecord(state.meta).serverId);
|
||||
for (const betting of pendingNationBettingOpens) {
|
||||
await persistNationBettingOpen(prisma, betting);
|
||||
}
|
||||
for (const finish of pendingNationBettingFinishes) {
|
||||
await persistNationBettingFinish(prisma, finish);
|
||||
await persistNationBettingFinish(prisma, finish, recordsFinalized);
|
||||
}
|
||||
|
||||
const meta = asRecord(state.meta);
|
||||
@@ -1464,7 +1467,7 @@ export const createDatabaseTurnHooks = async (
|
||||
const persistInheritancePointAdjustments = async (
|
||||
entries: typeof inheritancePointAdjustments
|
||||
): Promise<void> => {
|
||||
if (entries.length === 0) {
|
||||
if (recordsFinalized || entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
const grouped = new Map<string, { userId: string; key: string; amount: number }>();
|
||||
@@ -1490,7 +1493,7 @@ export const createDatabaseTurnHooks = async (
|
||||
}
|
||||
};
|
||||
const persistInheritanceLogs = async (entries: typeof pendingInheritanceLogs): Promise<void> => {
|
||||
if (entries.length === 0) {
|
||||
if (recordsFinalized || entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
await prisma.inheritanceLog.createMany({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { areSeasonRecordsFinalized } from './seasonRecords.js';
|
||||
import {
|
||||
asRecord,
|
||||
HALL_OF_FAME_TYPES,
|
||||
@@ -403,10 +404,15 @@ export const persistGeneralLifecycleEvents = async (
|
||||
data: { refreshScore: 0 },
|
||||
});
|
||||
|
||||
const recordsFinalized = await areSeasonRecordsFinalized(prisma, worldMeta.serverId);
|
||||
for (const event of events) {
|
||||
if (event.outcome === 'detached' || event.outcome === 'deleted') {
|
||||
await prisma.generalAccessLog.deleteMany({ where: { generalId: event.generalId } });
|
||||
}
|
||||
if (recordsFinalized) {
|
||||
if (event.outcome === 'retired') await persistPostRetirementRankValues(prisma, event);
|
||||
continue;
|
||||
}
|
||||
if (event.outcome !== 'deleted' && event.outcome !== 'retired') {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { areSeasonRecordsFinalized } from './seasonRecords.js';
|
||||
import { asNumber, asRecord, JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import { acquireGameSchemaAdvisoryXactLock, GamePrisma } from '@sammo-ts/infra';
|
||||
import {
|
||||
@@ -613,14 +614,20 @@ export const createGeneralFromJoin = async (options: {
|
||||
const inheritBonus = validateAndNormalizeBonus(input.inheritBonusStat);
|
||||
const inheritConstants = resolveInheritConstants(worldState);
|
||||
const worldMeta = asRecord(worldState.meta);
|
||||
const recordsFinalized = await areSeasonRecordsFinalized(db, worldMeta.serverId);
|
||||
const inheritRequiredPoint = calculateInheritanceCost(input, inheritConstants, inheritBonus);
|
||||
const currentInheritancePoint = await applyInheritanceUser(
|
||||
db,
|
||||
input.userId,
|
||||
worldState.currentYear,
|
||||
worldState.currentMonth
|
||||
);
|
||||
await ensureGameInheritanceBaseline(db, worldMeta, input.userId, currentInheritancePoint);
|
||||
if (recordsFinalized && inheritRequiredPoint > 0) {
|
||||
fail('BAD_REQUEST', '통일 이후에는 유산 포인트를 사용하는 생성 옵션을 적용할 수 없습니다.');
|
||||
}
|
||||
const currentInheritancePoint = recordsFinalized
|
||||
? (await db.inheritancePoint.findMany({ where: { userId: input.userId }, select: { value: true } })).reduce(
|
||||
(sum, row) => sum + row.value,
|
||||
0
|
||||
)
|
||||
: await applyInheritanceUser(db, input.userId, worldState.currentYear, worldState.currentMonth);
|
||||
if (!recordsFinalized) {
|
||||
await ensureGameInheritanceBaseline(db, worldMeta, input.userId, currentInheritancePoint);
|
||||
}
|
||||
if (currentInheritancePoint < inheritRequiredPoint) {
|
||||
fail('BAD_REQUEST', '유산 포인트가 부족합니다. 다시 가입해주세요!');
|
||||
}
|
||||
@@ -743,7 +750,7 @@ export const createGeneralFromJoin = async (options: {
|
||||
? input.ownerIconRevision
|
||||
: undefined;
|
||||
const nextInheritancePoint = currentInheritancePoint - inheritRequiredPoint;
|
||||
const restInheritanceBonus = await resolveRestInheritanceBonus(db, worldState, input.userId);
|
||||
const restInheritanceBonus = recordsFinalized ? 0 : await resolveRestInheritanceBonus(db, worldState, input.userId);
|
||||
const finalInheritancePoint = nextInheritancePoint + restInheritanceBonus;
|
||||
// Ref의 가오픈 삭제 대기는 정지된 게임 clock이 아니라 실제 요청 접수 시각부터 흐른다.
|
||||
// 미래 정식 오픈에 clock을 고정한 PREOPEN에서도 사용자가 가오픈 중 두 턴을 기다리면
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { DatabaseClient } from '@sammo-ts/infra';
|
||||
|
||||
/** 통일 대기/이민족전으로 시계가 재개되어도 확정된 기수의 기록은 다시 열지 않는다. */
|
||||
export const areSeasonRecordsFinalized = async (
|
||||
db: Pick<DatabaseClient, 'gameHistory'>,
|
||||
serverId: unknown
|
||||
): Promise<boolean> => {
|
||||
if (typeof serverId !== 'string' || !serverId.trim()) return false;
|
||||
const history = await db.gameHistory.findUnique({
|
||||
where: { serverId: serverId.trim() },
|
||||
select: { status: true },
|
||||
});
|
||||
return history?.status === 'COMPLETED';
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { areSeasonRecordsFinalized } from './seasonRecords.js';
|
||||
import { asRecord, HALL_OF_FAME_TYPES, resolveLegacyTextColor, type HallOfFameType } from '@sammo-ts/common';
|
||||
import {
|
||||
acquireGameSchemaAdvisoryXactLock,
|
||||
@@ -95,6 +96,8 @@ const claimGeneration = async (
|
||||
}
|
||||
return 'ALREADY_APPLIED';
|
||||
}
|
||||
// 이전 버전/이관 기수에 generation row가 없어도 통일 기록을 다시 정산하지 않는다.
|
||||
if (await areSeasonRecordsFinalized(transaction, input.serverId)) return 'ALREADY_APPLIED';
|
||||
await transaction.unificationFinalization.create({
|
||||
data: {
|
||||
generationKey: input.generationKey,
|
||||
|
||||
Reference in New Issue
Block a user