fix reserved turn permission constraints
This commit is contained in:
@@ -5,7 +5,7 @@ import { ITEM_KEYS, loadItemModules } from '@sammo-ts/logic';
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { buildBattleSimEnvironment } from '../../battleSim/environment.js';
|
||||
import { loadBattleSimTraitOptions } from '../../battleSim/simulatorOptions.js';
|
||||
import { buildTurnCommandTable } from '../../turns/commandTable.js';
|
||||
import { buildTurnCommandTable, evaluateReservedTurnPermission } from '../../turns/commandTable.js';
|
||||
import {
|
||||
parseReservedTurnArgs,
|
||||
TURN_COMMAND_NATION_COLORS,
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
shiftNationTurns,
|
||||
} from '../../turns/reservedTurns.js';
|
||||
import { getOwnedGeneral } from '../shared/general.js';
|
||||
import type { GameApiContext, GeneralRow, WorldStateRow } from '../../context.js';
|
||||
|
||||
const zPushAmount = z
|
||||
.number()
|
||||
@@ -80,6 +81,43 @@ const mutateReservedTurns = async <T>(mutation: () => Promise<T>): Promise<T> =>
|
||||
}
|
||||
};
|
||||
|
||||
const getReservationWorldState = async (ctx: GameApiContext): Promise<WorldStateRow> => {
|
||||
const worldState = await ctx.db.worldState.findFirst();
|
||||
if (!worldState) {
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
message: 'World state is not initialized.',
|
||||
});
|
||||
}
|
||||
return worldState;
|
||||
};
|
||||
|
||||
const assertReservedTurnPermission = async (
|
||||
worldState: WorldStateRow,
|
||||
general: GeneralRow,
|
||||
scope: 'general' | 'nation',
|
||||
action: string,
|
||||
args: Record<string, unknown>
|
||||
): Promise<void> => {
|
||||
const result = await evaluateReservedTurnPermission({
|
||||
worldState,
|
||||
general,
|
||||
scope,
|
||||
action,
|
||||
args,
|
||||
});
|
||||
if (result.kind === 'allow') {
|
||||
return;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: 'PRECONDITION_FAILED',
|
||||
message:
|
||||
result.kind === 'deny'
|
||||
? `예약 불가능한 커맨드 :${result.reason}`
|
||||
: '예약 권한을 확인할 정보가 부족합니다.',
|
||||
});
|
||||
};
|
||||
|
||||
export const turnsRouter = router({
|
||||
getCommandTable: authedProcedure
|
||||
.input(
|
||||
@@ -239,8 +277,10 @@ export const turnsRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await getOwnedGeneral(ctx, input.generalId);
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const args = await parseCommandArgs('general', input.action, input.args);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
await assertReservedTurnPermission(worldState, general, 'general', input.action, args);
|
||||
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
setGeneralTurn(ctx.db, input.generalId, input.turnIndex, input.action, args, input.expectedRevision)
|
||||
@@ -293,7 +333,7 @@ export const turnsRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await getOwnedGeneral(ctx, input.generalId);
|
||||
const general = await getOwnedGeneral(ctx, input.generalId);
|
||||
const updates = await Promise.all(
|
||||
input.entries.map(async (entry) => ({
|
||||
turnIndices: expandGeneralTurnIndices(entry.turnList),
|
||||
@@ -301,6 +341,16 @@ export const turnsRouter = router({
|
||||
args: await parseCommandArgs('general', entry.action, entry.args),
|
||||
}))
|
||||
);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
for (const update of updates) {
|
||||
await assertReservedTurnPermission(
|
||||
worldState,
|
||||
general,
|
||||
'general',
|
||||
update.action,
|
||||
update.args
|
||||
);
|
||||
}
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
setGeneralTurns(ctx.db, input.generalId, updates, input.expectedRevision)
|
||||
);
|
||||
@@ -335,6 +385,8 @@ export const turnsRouter = router({
|
||||
});
|
||||
}
|
||||
const args = await parseCommandArgs('nation', input.action, input.args);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
await assertReservedTurnPermission(worldState, general, 'nation', input.action, args);
|
||||
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
setNationTurn(
|
||||
@@ -451,6 +503,16 @@ export const turnsRouter = router({
|
||||
args: await parseCommandArgs('nation', entry.action, entry.args),
|
||||
}))
|
||||
);
|
||||
const worldState = await getReservationWorldState(ctx);
|
||||
for (const update of updates) {
|
||||
await assertReservedTurnPermission(
|
||||
worldState,
|
||||
general,
|
||||
'nation',
|
||||
update.action,
|
||||
update.args
|
||||
);
|
||||
}
|
||||
const snapshot = await mutateReservedTurns(() =>
|
||||
setNationTurns(
|
||||
ctx.db,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
City,
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
ConstraintResult,
|
||||
General,
|
||||
GeneralItemSlots,
|
||||
GeneralActionDefinition,
|
||||
@@ -225,6 +226,8 @@ const buildConstraintEnv = (worldState: WorldStateRow): Record<string, unknown>
|
||||
const scenarioMeta = asRecord(meta.scenarioMeta);
|
||||
const startYear = typeof scenarioMeta.startYear === 'number' ? scenarioMeta.startYear : undefined;
|
||||
const relYear = typeof startYear === 'number' ? worldState.currentYear - startYear : undefined;
|
||||
const joinModeRaw = config.join_mode ?? config.joinMode;
|
||||
const joinMode = typeof joinModeRaw === 'string' ? joinModeRaw : 'full';
|
||||
|
||||
return {
|
||||
currentYear: worldState.currentYear,
|
||||
@@ -233,18 +236,23 @@ const buildConstraintEnv = (worldState: WorldStateRow): Record<string, unknown>
|
||||
month: worldState.currentMonth,
|
||||
startYear,
|
||||
relYear,
|
||||
join_mode: joinMode,
|
||||
openingPartYear: resolveNumber(constValues, ['openingPartYear'], 0),
|
||||
};
|
||||
};
|
||||
|
||||
const mapGeneralRow = (row: GeneralRow): General => {
|
||||
const mapGeneralRow = (row: GeneralRow, missingKillturnFallback?: number): General => {
|
||||
const legacySlots: GeneralItemSlots = {
|
||||
horse: normalizeCode(row.horseCode),
|
||||
weapon: normalizeCode(row.weaponCode),
|
||||
book: normalizeCode(row.bookCode),
|
||||
item: normalizeCode(row.itemCode),
|
||||
};
|
||||
const meta = ensureGeneralMeta(asTriggerRecord(row.meta), row.id);
|
||||
const rawMeta = asTriggerRecord(row.meta);
|
||||
const meta =
|
||||
readMetaNumber(rawMeta, 'killturn') === null && missingKillturnFallback !== undefined
|
||||
? ({ ...rawMeta, killturn: missingKillturnFallback } as General['meta'])
|
||||
: ensureGeneralMeta(rawMeta, row.id);
|
||||
const itemInventory = readItemInventoryFromMeta(meta, legacySlots);
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -339,10 +347,16 @@ const buildStateView = (
|
||||
general: General,
|
||||
city: City | null,
|
||||
nation: Nation | null,
|
||||
generalList: General[] | null
|
||||
generalList: General[] | null,
|
||||
env: Record<string, unknown>
|
||||
): StateView => {
|
||||
const view = new MemoryStateView();
|
||||
view.set({ kind: 'general', id: general.id }, general);
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (value !== undefined) {
|
||||
view.set({ kind: 'env', key }, value);
|
||||
}
|
||||
}
|
||||
if (city) {
|
||||
view.set({ kind: 'city', id: city.id }, city);
|
||||
}
|
||||
@@ -485,14 +499,15 @@ export const buildTurnCommandTable = async (options: {
|
||||
const city = options.city ? mapCityRow(options.city) : null;
|
||||
const nation = options.nation ? mapNationRow(options.nation) : null;
|
||||
const generalList = options.nationGenerals ? options.nationGenerals.map(mapGeneralRow) : null;
|
||||
const view = buildStateView(general, city, nation, generalList);
|
||||
const constraintEnv = buildConstraintEnv(options.worldState);
|
||||
const view = buildStateView(general, city, nation, generalList, constraintEnv);
|
||||
|
||||
const ctx: ConstraintContext = {
|
||||
actorId: general.id,
|
||||
cityId: city?.id,
|
||||
nationId: nation?.id,
|
||||
args: {},
|
||||
env: buildConstraintEnv(options.worldState),
|
||||
env: constraintEnv,
|
||||
mode: 'precheck',
|
||||
};
|
||||
|
||||
@@ -516,3 +531,35 @@ export const buildTurnCommandTable = async (options: {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const evaluateReservedTurnPermission = async (options: {
|
||||
worldState: WorldStateRow;
|
||||
general: GeneralRow;
|
||||
scope: 'general' | 'nation';
|
||||
action: string;
|
||||
args: Record<string, unknown>;
|
||||
}): Promise<ConstraintResult> => {
|
||||
const specs = await loadTurnCommandSpecs();
|
||||
const spec = specs[options.scope].find((entry) => entry.key === options.action);
|
||||
if (!spec) {
|
||||
throw new Error(`Unknown ${options.scope} turn command: ${options.action}`);
|
||||
}
|
||||
|
||||
const definition = spec.createDefinition(buildCommandEnv(options.worldState));
|
||||
if (!definition.buildPermissionConstraints) {
|
||||
return { kind: 'allow' };
|
||||
}
|
||||
|
||||
const general = mapGeneralRow(options.general, 0);
|
||||
const env = buildConstraintEnv(options.worldState);
|
||||
const ctx: ConstraintContext = {
|
||||
actorId: general.id,
|
||||
cityId: general.cityId > 0 ? general.cityId : undefined,
|
||||
nationId: general.nationId > 0 ? general.nationId : undefined,
|
||||
args: options.args,
|
||||
env,
|
||||
mode: 'full',
|
||||
};
|
||||
const view = buildStateView(general, null, null, null, env);
|
||||
return evaluateConstraints(definition.buildPermissionConstraints(ctx, options.args), ctx, view);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user