플레이 감사 국가 생멸과 장기 tick 저장을 보완하고 중간 전달 준비
This commit is contained in:
@@ -178,3 +178,55 @@ export const initializeAuditDiplomacy = (world: InMemoryTurnWorld, observedAt =
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
/** 국가 생멸로 생성/제거된 관계를 기록한다. 행위자나 명령은 관측하지 못했다면 추정하지 않는다. */
|
||||
export const recordNationAuditDiplomacy = (
|
||||
world: InMemoryTurnWorld,
|
||||
nationIds: readonly number[],
|
||||
relations: readonly TurnDiplomacy[],
|
||||
operation: 'CREATED' | 'REMOVED',
|
||||
observedTick?: number
|
||||
): void => {
|
||||
const state = world.getState();
|
||||
const serverId = state.meta.serverId;
|
||||
if (typeof serverId !== 'string' || !serverId.trim()) return;
|
||||
const targets = new Set(nationIds.filter((id) => id > 0));
|
||||
const observed = relations
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.fromNationId > 0 &&
|
||||
entry.toNationId > 0 &&
|
||||
(targets.has(entry.fromNationId) || targets.has(entry.toNationId))
|
||||
)
|
||||
.sort((left, right) => left.fromNationId - right.fromNationId || left.toNationId - right.toNationId);
|
||||
if (!observed.length) return;
|
||||
// 전역 순번은 실행 identity에만 사용한다. 한 사건 묶음 안의 순서는 방향별 local ordinal이다.
|
||||
const executionId = `nation-relations:${world.nextAuditOrdinal()}`;
|
||||
const clock = world.getGameClockState();
|
||||
for (const [index, entry] of observed.entries()) {
|
||||
const value = { state: entry.state, term: entry.term, dead: entry.dead };
|
||||
world.queueAuditDiplomacy({
|
||||
schemaVersion: 1,
|
||||
serverId,
|
||||
srcNationId: entry.fromNationId,
|
||||
destNationId: entry.toNationId,
|
||||
category: 'RELATION',
|
||||
source: 'ENGINE',
|
||||
eventType: `NATION_RELATION_${operation}`,
|
||||
documentId: null,
|
||||
documentHash: null,
|
||||
previousDocumentId: null,
|
||||
year: state.currentYear,
|
||||
month: state.currentMonth,
|
||||
tick: BigInt(observedTick ?? clock.tick),
|
||||
clockRevision: BigInt(clock.revision),
|
||||
executionId,
|
||||
ordinal: index + 1,
|
||||
requestId: null,
|
||||
inputSequence: null,
|
||||
actor: null,
|
||||
before: operation === 'REMOVED' ? value : null,
|
||||
after: operation === 'CREATED' ? value : null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,7 +28,8 @@ export const persistAuditMonth = async (
|
||||
!Number.isInteger(snapshot.year) ||
|
||||
!Number.isInteger(snapshot.month) ||
|
||||
snapshot.month < 1 ||
|
||||
snapshot.month > 12
|
||||
snapshot.month > 12 ||
|
||||
(snapshot.tick !== null && (!Number.isSafeInteger(snapshot.tick) || snapshot.tick < 0))
|
||||
) {
|
||||
throw new Error('Invalid play audit month identity');
|
||||
}
|
||||
@@ -42,7 +43,7 @@ export const persistAuditMonth = async (
|
||||
year: snapshot.year,
|
||||
month: snapshot.month,
|
||||
kind: snapshot.kind,
|
||||
tick: snapshot.tick,
|
||||
tick: snapshot.tick === null ? null : BigInt(snapshot.tick),
|
||||
settlementsComplete: snapshot.settlementsComplete,
|
||||
hash,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,8 @@ export const persistAuditPolicies = async (
|
||||
): Promise<void> => {
|
||||
for (let offset = 0; offset < policies.length; offset += 200) {
|
||||
const batch = policies.slice(offset, offset + 200).map((policy) => {
|
||||
if (!Number.isSafeInteger(policy.tick) || policy.tick < 0)
|
||||
throw new Error('Invalid play audit policy tick');
|
||||
if (policy.requestId && !command) throw new Error('Play audit policy input event context missing');
|
||||
if (
|
||||
policy.requestId &&
|
||||
@@ -18,6 +20,7 @@ export const persistAuditPolicies = async (
|
||||
}
|
||||
return {
|
||||
...policy,
|
||||
tick: BigInt(policy.tick),
|
||||
inputSequence: policy.requestId && command ? command.sequence : null,
|
||||
actor: policy.actor ? (JSON.parse(JSON.stringify(policy.actor)) as InputJsonValue) : GamePrisma.DbNull,
|
||||
before: policy.before
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { recordTurnAuditDiplomacy, type AuditDiplomacyAction } from '../playAudit/diplomacy.js';
|
||||
import {
|
||||
recordTurnAuditDiplomacy,
|
||||
recordNationAuditDiplomacy,
|
||||
type AuditDiplomacyAction,
|
||||
} from '../playAudit/diplomacy.js';
|
||||
import type { AuditDiplomacyEventDraft } from '@sammo-ts/infra';
|
||||
import { initializeNationAuditPolicies, type PendingAuditPolicy } from '../playAudit/policy.js';
|
||||
import type { PendingAuditMonth } from '../playAudit/persistence.js';
|
||||
@@ -1635,6 +1639,7 @@ export class InMemoryTurnWorld {
|
||||
this.createdNationIds.add(nation.id);
|
||||
this.ensureDiplomacyMatrix();
|
||||
initializeNationAuditPolicies(this, nation.id);
|
||||
recordNationAuditDiplomacy(this, [nation.id], this.collectNationDiplomacy([nation.id]), 'CREATED');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1723,7 +1728,7 @@ export class InMemoryTurnWorld {
|
||||
return true;
|
||||
}
|
||||
|
||||
removeNation(id: number): boolean {
|
||||
removeNation(id: number, auditTick?: number): boolean {
|
||||
if (!this.nations.has(id)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1731,13 +1736,16 @@ export class InMemoryTurnWorld {
|
||||
this.dirtyNationIds.delete(id);
|
||||
this.createdNationIds.delete(id);
|
||||
this.deletedNationIds.add(id);
|
||||
const removedRelations: TurnDiplomacy[] = [];
|
||||
for (const [key, entry] of this.diplomacy) {
|
||||
if (entry.fromNationId === id || entry.toNationId === id) {
|
||||
removedRelations.push(entry);
|
||||
this.diplomacy.delete(key);
|
||||
this.dirtyDiplomacyKeys.delete(key);
|
||||
this.createdDiplomacyKeys.delete(key);
|
||||
}
|
||||
}
|
||||
recordNationAuditDiplomacy(this, [id], removedRelations, 'REMOVED', auditTick);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2100,7 +2108,7 @@ export class InMemoryTurnWorld {
|
||||
this.createdGeneralIds.add(createdGeneral.id);
|
||||
}
|
||||
if (result.created.nations) {
|
||||
let addedNation = false;
|
||||
const addedNationIds: number[] = [];
|
||||
for (const createdNation of result.created.nations) {
|
||||
if (this.nations.has(createdNation.id)) {
|
||||
continue;
|
||||
@@ -2108,10 +2116,18 @@ export class InMemoryTurnWorld {
|
||||
this.nations.set(createdNation.id, { ...createdNation });
|
||||
this.dirtyNationIds.add(createdNation.id);
|
||||
this.createdNationIds.add(createdNation.id);
|
||||
addedNation = true;
|
||||
addedNationIds.push(createdNation.id);
|
||||
}
|
||||
if (addedNation) {
|
||||
if (addedNationIds.length) {
|
||||
this.ensureDiplomacyMatrix();
|
||||
for (const id of addedNationIds) initializeNationAuditPolicies(this, id);
|
||||
recordNationAuditDiplomacy(
|
||||
this,
|
||||
addedNationIds,
|
||||
this.collectNationDiplomacy(addedNationIds),
|
||||
'CREATED',
|
||||
executionTick
|
||||
);
|
||||
}
|
||||
}
|
||||
if (result.created.troops) {
|
||||
@@ -2133,7 +2149,7 @@ export class InMemoryTurnWorld {
|
||||
if (result.successorlessNationId !== undefined) {
|
||||
// 사망 군주도 삭제 전 archive의 장수 목록과 멸망 로그에 포함한다.
|
||||
this.generals.set(currentGeneral.id, result.general ?? currentGeneral);
|
||||
this.dissolveNationWithoutSuccessor(result.successorlessNationId, currentGeneral.id);
|
||||
this.dissolveNationWithoutSuccessor(result.successorlessNationId, currentGeneral.id, executionTick);
|
||||
}
|
||||
if (result.deleted?.general) {
|
||||
this.removeGeneral(currentGeneral.id);
|
||||
@@ -2142,7 +2158,7 @@ export class InMemoryTurnWorld {
|
||||
this.lifecycleEvents.push(result.lifecycleEvent);
|
||||
}
|
||||
|
||||
this.removeCollapsedNations();
|
||||
this.removeCollapsedNations(executionTick);
|
||||
|
||||
return {
|
||||
nextTurnAt,
|
||||
@@ -2343,7 +2359,7 @@ export class InMemoryTurnWorld {
|
||||
return changes;
|
||||
}
|
||||
|
||||
dissolveNationWithoutSuccessor(nationId: number, dyingLordId?: number): boolean {
|
||||
dissolveNationWithoutSuccessor(nationId: number, dyingLordId?: number, auditTick?: number): boolean {
|
||||
const nation = this.nations.get(nationId);
|
||||
if (!nation) {
|
||||
return false;
|
||||
@@ -2398,10 +2414,10 @@ export class InMemoryTurnWorld {
|
||||
}
|
||||
// 과거 누락으로 군주가 이미 삭제된 국가의 운영 복구도 같은 정산을 쓴다.
|
||||
if (dyingLordId === undefined) pushHistory();
|
||||
return this.collapseNation(nationId);
|
||||
return this.collapseNation(nationId, auditTick);
|
||||
}
|
||||
|
||||
collapseNation(nationId: number): boolean {
|
||||
collapseNation(nationId: number, auditTick?: number): boolean {
|
||||
const nation = this.nations.get(nationId);
|
||||
if (!nation) {
|
||||
return false;
|
||||
@@ -2480,11 +2496,11 @@ export class InMemoryTurnWorld {
|
||||
this.removeTroop(troop.id);
|
||||
}
|
||||
}
|
||||
this.removeNation(nationId);
|
||||
this.removeNation(nationId, auditTick);
|
||||
return true;
|
||||
}
|
||||
|
||||
private removeCollapsedNations(): void {
|
||||
private removeCollapsedNations(auditTick?: number): void {
|
||||
const collapsedNationIds: number[] = [];
|
||||
for (const nation of this.nations.values()) {
|
||||
if (nation.id <= 0) {
|
||||
@@ -2502,10 +2518,26 @@ export class InMemoryTurnWorld {
|
||||
}
|
||||
|
||||
for (const nationId of collapsedNationIds) {
|
||||
this.collapseNation(nationId);
|
||||
this.collapseNation(nationId, auditTick);
|
||||
}
|
||||
}
|
||||
|
||||
/** 감사 때문에 전체 관계 matrix를 복제하지 않고 대상 국가에 연결된 행만 가져온다. */
|
||||
private collectNationDiplomacy(nationIds: readonly number[]): TurnDiplomacy[] {
|
||||
const relations = new Map<string, TurnDiplomacy>();
|
||||
for (const nationId of nationIds) {
|
||||
if (nationId <= 0) continue;
|
||||
for (const otherId of this.nations.keys()) {
|
||||
if (otherId <= 0 || otherId === nationId) continue;
|
||||
for (const key of [buildDiplomacyKey(nationId, otherId), buildDiplomacyKey(otherId, nationId)]) {
|
||||
const relation = this.diplomacy.get(key);
|
||||
if (relation) relations.set(key, relation);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(relations.values());
|
||||
}
|
||||
|
||||
private ensureDiplomacyMatrix(): void {
|
||||
const nationIds = Array.from(this.nations.keys());
|
||||
for (const srcNationId of nationIds) {
|
||||
|
||||
Reference in New Issue
Block a user