197 lines
7.5 KiB
TypeScript
197 lines
7.5 KiB
TypeScript
import { TRPCError } from '@trpc/server';
|
|
import { z } from 'zod';
|
|
import type { GamePrisma } from '@sammo-ts/infra';
|
|
import { auditProcedure, monthOrdinal, readAudit, readAuditWorld, zAuditMonth } from './shared.js';
|
|
|
|
const zId = z.string().regex(/^[a-f0-9]{64}$/);
|
|
const zTick = z
|
|
.string()
|
|
.regex(/^\d{1,16}$/)
|
|
.refine((value) => BigInt(value) <= BigInt(Number.MAX_SAFE_INTEGER));
|
|
const zSummary = z.object({
|
|
schemaVersion: z.literal(1),
|
|
coverage: z.literal('PROCEDURES'),
|
|
clockRevision: z.number().int(),
|
|
codeVersion: z.string().nullable(),
|
|
policyRefs: z.object({
|
|
NPC_VALUES: zId.optional(),
|
|
NPC_NATION_PRIORITY: zId.optional(),
|
|
NPC_GENERAL_PRIORITY: zId.optional(),
|
|
DEFENCE: zId.optional(),
|
|
}),
|
|
requestedAction: z.string(),
|
|
selectedAction: z.string().nullable(),
|
|
selectedReason: z.string().nullable(),
|
|
executedAction: z.string(),
|
|
completed: z.boolean().nullable(),
|
|
usedFallback: z.boolean(),
|
|
blockedReason: z.string().nullable(),
|
|
});
|
|
const zValue = z.union([
|
|
z.string(),
|
|
z.number(),
|
|
z.boolean(),
|
|
z.null(),
|
|
z.object({ entityId: z.number() }),
|
|
z.object({ unprojected: z.literal(true) }),
|
|
]);
|
|
const zStep = z.intersection(
|
|
z.object({
|
|
sequence: z.number().int().nonnegative(),
|
|
phase: z.enum(['general', 'nation']),
|
|
generalId: z.number().int(),
|
|
nationId: z.number().int(),
|
|
cityId: z.number().int(),
|
|
npcState: z.number().int(),
|
|
year: z.number().int(),
|
|
month: z.number().int(),
|
|
tick: z.number().nullable(),
|
|
}),
|
|
z.discriminatedUnion('kind', [
|
|
z.object({ kind: z.literal('DECISION_START'), reservedAction: z.string() }),
|
|
z.object({ kind: z.literal('DECISION_END'), action: z.string().nullable(), reason: z.string().nullable() }),
|
|
z.object({ kind: z.literal('DECISION_ERROR') }),
|
|
z.object({ kind: z.literal('PROCEDURE_START'), procedure: z.string() }),
|
|
z.object({
|
|
kind: z.literal('PROCEDURE_END'),
|
|
procedure: z.string(),
|
|
action: z.string().nullable(),
|
|
reason: z.string().nullable(),
|
|
}),
|
|
z.object({
|
|
kind: z.literal('PROCEDURE_SKIP'),
|
|
procedure: z.string(),
|
|
reason: z.enum(['POLICY', 'AUTOMATION', 'NO_HANDLER']),
|
|
}),
|
|
z.object({
|
|
kind: z.literal('CANDIDATE'),
|
|
action: z.string(),
|
|
result: z.enum(['INVALID_ARGS', 'allow', 'deny', 'unknown']),
|
|
constraint: z.string().nullable(),
|
|
}),
|
|
z.object({
|
|
kind: z.literal('RNG'),
|
|
method: z.string(),
|
|
parameters: z.array(z.number()).nullable(),
|
|
result: z.union([zValue, z.array(zValue)]),
|
|
}),
|
|
])
|
|
);
|
|
const select = {
|
|
id: true,
|
|
executionId: true,
|
|
phase: true,
|
|
generalId: true,
|
|
nationId: true,
|
|
cityId: true,
|
|
npcState: true,
|
|
year: true,
|
|
month: true,
|
|
tick: true,
|
|
stepCount: true,
|
|
summary: true,
|
|
createdAt: true,
|
|
} satisfies GamePrisma.PlayAuditDecisionSelect;
|
|
const project = (row: GamePrisma.PlayAuditDecisionGetPayload<{ select: typeof select }>) => ({
|
|
...row,
|
|
phase: z.enum(['general', 'nation']).parse(row.phase),
|
|
tick: row.tick.toString(),
|
|
summary: zSummary.parse(row.summary),
|
|
});
|
|
export const decisionHistory = auditProcedure
|
|
.input(
|
|
z
|
|
.object({
|
|
generalId: z.number().int().positive().max(2147483647),
|
|
month: zAuditMonth.omit({ kind: true }).optional(),
|
|
phase: z.enum(['general', 'nation']).optional(),
|
|
cursor: z.object({ tick: zTick, id: zId }).strict().optional(),
|
|
limit: z.number().int().min(1).max(200).default(50),
|
|
})
|
|
.strict()
|
|
)
|
|
.query(({ ctx, input }) =>
|
|
readAudit(ctx, async (tx) => {
|
|
const world = await readAuditWorld(tx);
|
|
const month = input.month ?? { year: world.year, month: world.month };
|
|
const ordinal = monthOrdinal(month.year, month.month);
|
|
if (
|
|
ordinal < monthOrdinal(world.startYear, world.startMonth) ||
|
|
ordinal > monthOrdinal(world.year, world.month)
|
|
)
|
|
throw new TRPCError({ code: 'BAD_REQUEST', message: '현재 기수 안의 결정 조회 월을 선택해 주세요.' });
|
|
const rows = world.serverId
|
|
? await tx.playAuditDecision.findMany({
|
|
where: {
|
|
serverId: world.serverId,
|
|
generalId: input.generalId,
|
|
year: month.year,
|
|
month: month.month,
|
|
phase: input.phase,
|
|
...(input.cursor
|
|
? {
|
|
OR: [
|
|
{ tick: { lt: BigInt(input.cursor.tick) } },
|
|
{ tick: BigInt(input.cursor.tick), id: { lt: input.cursor.id } },
|
|
],
|
|
}
|
|
: {}),
|
|
},
|
|
orderBy: [{ tick: 'desc' }, { id: 'desc' }],
|
|
take: input.limit + 1,
|
|
select,
|
|
})
|
|
: [];
|
|
const last = rows[input.limit - 1];
|
|
return {
|
|
...world,
|
|
month,
|
|
coverage: world.serverId ? ('PROCEDURES_ONLY' as const) : ('IDENTITY_MISSING' as const),
|
|
items: rows.slice(0, input.limit).map(project),
|
|
nextCursor: rows.length > input.limit && last ? { tick: last.tick.toString(), id: last.id } : null,
|
|
};
|
|
})
|
|
);
|
|
export const decisionDetail = auditProcedure
|
|
.input(
|
|
z
|
|
.object({
|
|
id: zId,
|
|
generalId: z.number().int().positive().max(2147483647),
|
|
cursor: z.number().int().nonnegative().max(2147483647).optional(),
|
|
limit: z.number().int().min(1).max(4).default(1),
|
|
})
|
|
.strict()
|
|
)
|
|
.query(({ ctx, input }) =>
|
|
readAudit(ctx, async (tx) => {
|
|
const world = await readAuditWorld(tx);
|
|
const row = world.serverId
|
|
? await tx.playAuditDecision.findFirst({
|
|
where: { id: input.id, generalId: input.generalId, serverId: world.serverId },
|
|
select,
|
|
})
|
|
: null;
|
|
if (!row)
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: '현재 기수에서 해당 결정 기록을 찾을 수 없습니다.' });
|
|
const chunks = await tx.playAuditDecisionChunk.findMany({
|
|
where: { decisionId: row.id, ordinal: input.cursor === undefined ? undefined : { gt: input.cursor } },
|
|
orderBy: { ordinal: 'asc' },
|
|
take: input.limit,
|
|
select: { ordinal: true, steps: true },
|
|
});
|
|
return {
|
|
...world,
|
|
decision: project(row),
|
|
chunks: chunks.slice(0, input.limit).map((chunk) => ({
|
|
ordinal: chunk.ordinal,
|
|
steps: z.array(zStep).max(128).parse(chunk.steps),
|
|
})),
|
|
nextCursor:
|
|
chunks.length && chunks.at(-1)!.ordinal + 1 < Math.ceil(row.stepCount / 128)
|
|
? chunks.at(-1)!.ordinal
|
|
: null,
|
|
};
|
|
})
|
|
);
|