fix reserved turn bulk and repeat parity

This commit is contained in:
2026-07-26 18:41:51 +00:00
parent 3cd1ad3b7f
commit 7a18bb48f2
4 changed files with 507 additions and 29 deletions
+150 -9
View File
@@ -15,23 +15,42 @@ import {
MAX_GENERAL_TURNS,
MAX_NATION_TURNS,
ReservedTurnRevisionConflictError,
expandGeneralTurnIndices,
getGeneralTurnSnapshot,
getNationTurnSnapshot,
repeatGeneralTurns,
repeatNationTurns,
setGeneralTurn,
setGeneralTurns,
setNationTurn,
setNationTurns,
shiftGeneralTurns,
shiftNationTurns,
} from '../../turns/reservedTurns.js';
import { getOwnedGeneral } from '../shared/general.js';
const buildShiftAmountSchema = (maxTurns: number) =>
const zPushAmount = z
.number()
.int()
.min(-12)
.max(12)
.refine((value) => value !== 0, {
message: 'Amount must be non-zero.',
});
const zRepeatAmount = z.number().int().min(1).max(12);
const buildTurnListSchema = (minimum: number, maximum: number) =>
z
.number()
.int()
.min(-(maxTurns - 1))
.max(maxTurns - 1)
.refine((value) => value !== 0, {
message: 'Amount must be non-zero.',
.array(z.number().int().min(minimum).max(maximum))
.min(1);
const buildBulkEntrySchema = (turnList: z.ZodType<number[]>) =>
z
.object({
turnList,
action: z.string().min(1),
args: z.unknown().optional(),
});
const parseCommandArgs = async (scope: 'general' | 'nation', action: string, args: unknown) => {
@@ -232,7 +251,7 @@ export const turnsRouter = router({
.input(
z.object({
generalId: z.number().int().positive(),
amount: buildShiftAmountSchema(MAX_GENERAL_TURNS),
amount: zPushAmount,
expectedRevision: z.number().int().nonnegative(),
})
)
@@ -244,6 +263,49 @@ export const turnsRouter = router({
);
return { ok: true, ...snapshot };
}),
repeatGeneral: authedProcedure
.input(
z.object({
generalId: z.number().int().positive(),
amount: zRepeatAmount,
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
await getOwnedGeneral(ctx, input.generalId);
const snapshot = await mutateReservedTurns(() =>
repeatGeneralTurns(ctx.db, input.generalId, input.amount, input.expectedRevision)
);
return { ok: true, ...snapshot };
}),
setGeneralBulk: authedProcedure
.input(
z.object({
generalId: z.number().int().positive(),
entries: z
.array(
buildBulkEntrySchema(
buildTurnListSchema(-3, MAX_GENERAL_TURNS - 1)
)
)
.min(1),
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
await getOwnedGeneral(ctx, input.generalId);
const updates = await Promise.all(
input.entries.map(async (entry) => ({
turnIndices: expandGeneralTurnIndices(entry.turnList),
action: entry.action,
args: await parseCommandArgs('general', entry.action, entry.args),
}))
);
const snapshot = await mutateReservedTurns(() =>
setGeneralTurns(ctx.db, input.generalId, updates, input.expectedRevision)
);
return { ok: true, ...snapshot };
}),
setNation: authedProcedure
.input(
z.object({
@@ -291,7 +353,7 @@ export const turnsRouter = router({
.input(
z.object({
generalId: z.number().int().positive(),
amount: buildShiftAmountSchema(MAX_NATION_TURNS),
amount: zPushAmount,
expectedRevision: z.number().int().nonnegative(),
})
)
@@ -321,5 +383,84 @@ export const turnsRouter = router({
);
return { ok: true, ...snapshot };
}),
repeatNation: authedProcedure
.input(
z.object({
generalId: z.number().int().positive(),
amount: zRepeatAmount,
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
const general = await getOwnedGeneral(ctx, input.generalId);
if (general.nationId <= 0) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: 'General is not part of a nation.',
});
}
if (general.officerLevel < 5) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'General is not an officer.',
});
}
const snapshot = await mutateReservedTurns(() =>
repeatNationTurns(
ctx.db,
general.nationId,
general.officerLevel,
input.amount,
input.expectedRevision
)
);
return { ok: true, ...snapshot };
}),
setNationBulk: authedProcedure
.input(
z.object({
generalId: z.number().int().positive(),
entries: z
.array(
buildBulkEntrySchema(
buildTurnListSchema(0, MAX_NATION_TURNS - 1)
)
)
.min(1),
expectedRevision: z.number().int().nonnegative(),
})
)
.mutation(async ({ ctx, input }) => {
const general = await getOwnedGeneral(ctx, input.generalId);
if (general.nationId <= 0) {
throw new TRPCError({
code: 'PRECONDITION_FAILED',
message: 'General is not part of a nation.',
});
}
if (general.officerLevel < 5) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'General is not an officer.',
});
}
const updates = await Promise.all(
input.entries.map(async (entry) => ({
turnIndices: entry.turnList,
action: entry.action,
args: await parseCommandArgs('nation', entry.action, entry.args),
}))
);
const snapshot = await mutateReservedTurns(() =>
setNationTurns(
ctx.db,
general.nationId,
general.officerLevel,
updates,
input.expectedRevision
)
);
return { ok: true, ...snapshot };
}),
}),
});