fix(api): align scenario 2601 reference data

This commit is contained in:
2026-08-04 05:29:12 +00:00
parent 87965a39d6
commit 9cfeaed3fe
10 changed files with 212 additions and 78 deletions
+4
View File
@@ -1,5 +1,7 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common';
import { zWorldStateConfig, zWorldStateMeta } from '../../context.js'; import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
import { isSelectionPoolWorld, resolveSelectionMaxGeneral } from '../../services/selectPool.js'; import { isSelectionPoolWorld, resolveSelectionMaxGeneral } from '../../services/selectPool.js';
import { procedure, router } from '../../trpc.js'; import { procedure, router } from '../../trpc.js';
@@ -23,6 +25,7 @@ export const lobbyRouter = router({
const userCnt = await ctx.db.general.count({ where: { npcState: { lt: 2 } } }); const userCnt = await ctx.db.general.count({ where: { npcState: { lt: 2 } } });
const npcCnt = await ctx.db.general.count({ where: { npcState: { gte: 2 } } }); const npcCnt = await ctx.db.general.count({ where: { npcState: { gte: 2 } } });
const nationCnt = await ctx.db.nation.count({ where: { level: { gt: 0 } } }); const nationCnt = await ctx.db.nation.count({ where: { level: { gt: 0 } } });
const scenarioTitle = asRecord(asRecord(rawWorldState.meta).scenarioMeta).title;
let myGeneral = null; let myGeneral = null;
if (ctx.auth?.user.id) { if (ctx.auth?.user.id) {
@@ -55,6 +58,7 @@ export const lobbyRouter = router({
isUnited: worldState.meta.isunited ?? worldState.meta.isUnited ?? 0, isUnited: worldState.meta.isunited ?? worldState.meta.isUnited ?? 0,
selectionPoolEnabled: isSelectionPoolWorld(rawWorldState), selectionPoolEnabled: isSelectionPoolWorld(rawWorldState),
npcPossessionEnabled: worldState.config.npcMode === 1, npcPossessionEnabled: worldState.config.npcMode === 1,
scenarioTitle: typeof scenarioTitle === 'string' ? scenarioTitle : '',
myGeneral, myGeneral,
}; };
}), }),
@@ -27,6 +27,8 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
select: { select: {
id: true, id: true,
name: true, name: true,
picture: true,
imageServer: true,
npcState: true, npcState: true,
officerLevel: true, officerLevel: true,
cityId: true, cityId: true,
@@ -43,6 +45,16 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
crew: true, crew: true,
train: true, train: true,
atmos: true, atmos: true,
age: true,
crewTypeId: true,
weaponCode: true,
bookCode: true,
horseCode: true,
itemCode: true,
personalCode: true,
specialCode: true,
special2Code: true,
meta: true,
}, },
orderBy: { id: 'asc' }, orderBy: { id: 'asc' },
}), }),
@@ -79,29 +91,62 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
} }
} }
const generals = generalRows.map((general) => ({ const generals = generalRows.map((general) => {
id: general.id, const meta =
name: general.name, general.meta && typeof general.meta === 'object' && !Array.isArray(general.meta)
npcState: general.npcState, ? (general.meta as Record<string, unknown>)
officerLevel: general.officerLevel, : {};
cityId: general.cityId, const metaNumber = (key: string): number => {
turnTime: formatDateTime(general.turnTime), const value = meta[key];
recentWar: formatDateTime(general.recentWarTime), return typeof value === 'number' && Number.isFinite(value) ? value : 0;
warnum: battleCountMap.get(general.id) ?? 0, };
stats: { return {
leadership: general.leadership, id: general.id,
strength: general.strength, name: general.name,
intelligence: general.intel, picture: general.picture,
}, imageServer: general.imageServer,
experience: general.experience, npcState: general.npcState,
dedication: general.dedication, officerLevel: general.officerLevel,
injury: general.injury, cityId: general.cityId,
gold: general.gold, turnTime: formatDateTime(general.turnTime),
rice: general.rice, recentWar: formatDateTime(general.recentWarTime),
crew: general.crew, warnum: battleCountMap.get(general.id) ?? 0,
train: general.train, stats: {
atmos: general.atmos, leadership: general.leadership,
})); strength: general.strength,
intelligence: general.intel,
},
experience: general.experience,
dedication: general.dedication,
injury: general.injury,
gold: general.gold,
rice: general.rice,
crew: general.crew,
train: general.train,
atmos: general.atmos,
age: general.age,
crewTypeId: general.crewTypeId,
equipment: {
weapon: general.weaponCode,
book: general.bookCode,
horse: general.horseCode,
item: general.itemCode,
},
traits: {
personal: general.personalCode,
specialDomestic: general.specialCode,
specialWar: general.special2Code,
},
battleStats: {
kills: metaNumber('rank_killnum') || metaNumber('killnum'),
deaths: metaNumber('deathnum'),
fire: metaNumber('firenum'),
killCrew: metaNumber('killcrew'),
deathCrew: metaNumber('deathcrew'),
dex: [1, 2, 3, 4, 5].map((index) => metaNumber(`dex${index}`)),
},
};
});
return { return {
me: { me: {
@@ -5,7 +5,13 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
import { authedProcedure } from '../../../trpc.js'; import { authedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js'; import { getMyGeneral } from '../../shared/general.js';
import { assertNationAccess, resolveNationPermission, zGeneralLogType, type GeneralLogType } from '../shared.js'; import {
assertNationAccess,
formatDateTime,
resolveNationPermission,
zGeneralLogType,
type GeneralLogType,
} from '../shared.js';
export const getGeneralLog = authedProcedure export const getGeneralLog = authedProcedure
.input( .input(
@@ -75,6 +81,9 @@ export const getGeneralLog = authedProcedure
logs: logs.map((entry) => ({ logs: logs.map((entry) => ({
id: entry.id, id: entry.id,
text: entry.text, text: entry.text,
year: entry.year,
month: entry.month,
createdAt: formatDateTime(entry.createdAt),
})), })),
}; };
}); });
@@ -30,7 +30,7 @@ export const getNationInfo = authedProcedure.query(async ({ ctx }) => {
nationId: me.nationId, nationId: me.nationId,
}, },
select: { id: true, year: true, month: true, text: true }, select: { id: true, year: true, month: true, text: true },
orderBy: { id: 'asc' }, orderBy: { id: 'desc' },
}), }),
]); ]);
if (!nation) { if (!nation) {
@@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common'; import { asRecord } from '@sammo-ts/common';
import { loadUnitSetDefinitionByName } from '../../../battleSim/unitSetLoader.js';
import { accessAuthedProcedure } from '../../../trpc.js'; import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js'; import { getMyGeneral } from '../../shared/general.js';
import { assertNationAccess, resolveNationPermission } from '../shared.js'; import { assertNationAccess, resolveNationPermission } from '../shared.js';
@@ -41,7 +42,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
}); });
} }
const [cities, troops, generalRows] = await Promise.all([ const [cities, troops, generalRows, worldState] = await Promise.all([
ctx.db.city.findMany({ select: { id: true, name: true } }), ctx.db.city.findMany({ select: { id: true, name: true } }),
ctx.db.troop.findMany({ ctx.db.troop.findMany({
where: { nationId: me.nationId }, where: { nationId: me.nationId },
@@ -51,7 +52,14 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
where: { nationId: me.nationId }, where: { nationId: me.nationId },
orderBy: [{ turnTime: 'asc' }, { id: 'asc' }], orderBy: [{ turnTime: 'asc' }, { id: 'asc' }],
}), }),
ctx.db.worldState.findFirst({ select: { config: true } }),
]); ]);
const worldConfig = asRecord(worldState?.config);
const environment = asRecord(worldConfig.environment ?? worldConfig.map);
const unitSetName =
typeof environment.unitSet === 'string' && environment.unitSet.trim() ? environment.unitSet : ctx.profile.id;
const unitSet = await loadUnitSetDefinitionByName(unitSetName);
const crewTypeNames = new Map((unitSet.crewTypes ?? []).map((crewType) => [crewType.id, crewType.name]));
const generalIds = generalRows.map((general) => general.id); const generalIds = generalRows.map((general) => general.id);
const turns = generalIds.length const turns = generalIds.length
? await ctx.db.generalTurn.findMany({ ? await ctx.db.generalTurn.findMany({
@@ -92,6 +100,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
defenceTrain, defenceTrain,
defenceTrainText: defenceTrainText(defenceTrain), defenceTrainText: defenceTrainText(defenceTrain),
crewTypeId: general.crewTypeId, crewTypeId: general.crewTypeId,
crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
crew: general.crew, crew: general.crew,
train: general.train, train: general.train,
atmos: general.atmos, atmos: general.atmos,
@@ -1,7 +1,14 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common'; import { asRecord } from '@sammo-ts/common';
import { getGoldIncome, getOutcome, getRiceIncome, getWallIncome, getWarGoldIncome, type NationIncomeContext } from '@sammo-ts/logic'; import {
getGoldIncome,
getOutcome,
getRiceIncome,
getWallIncome,
getWarGoldIncome,
type NationIncomeContext,
} from '@sammo-ts/logic';
import { accessAuthedProcedure } from '../../../trpc.js'; import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js'; import { getMyGeneral } from '../../shared/general.js';
@@ -171,13 +178,7 @@ export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
const cityStatsByNation = new Map<number, { popSum: number; valueSum: number; maxSum: number }>(); const cityStatsByNation = new Map<number, { popSum: number; valueSum: number; maxSum: number }>();
for (const city of cityRows) { for (const city of cityRows) {
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 }; const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
const valueSum = const valueSum = city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
city.population +
city.agriculture +
city.commerce +
city.security +
city.wall +
city.defence;
const maxSum = const maxSum =
city.populationMax + city.populationMax +
city.agricultureMax + city.agricultureMax +
@@ -222,22 +223,24 @@ export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
); );
} }
const nationsList = nationRows.map((nationItem) => { const nationsList = nationRows
const diplomacy = .filter((nationItem) => nationItem.id > 0)
nationItem.id === nation.id .map((nationItem) => {
? { state: 7, term: null } const diplomacy =
: diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 }; nationItem.id === nation.id
return { ? { state: 7, term: null }
id: nationItem.id, : (diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 });
name: nationItem.name, return {
color: nationItem.color, id: nationItem.id,
level: nationItem.level, name: nationItem.name,
power: powerByNation.get(nationItem.id) ?? 0, color: nationItem.color,
generalCount: generalCountMap.get(nationItem.id) ?? 0, level: nationItem.level,
cityCount: cityCountMap.get(nationItem.id) ?? 0, power: powerByNation.get(nationItem.id) ?? 0,
diplomacy, generalCount: generalCountMap.get(nationItem.id) ?? 0,
}; cityCount: cityCountMap.get(nationItem.id) ?? 0,
}); diplomacy,
};
});
const nationCities = cityRows.filter((city) => city.nationId === nation.id); const nationCities = cityRows.filter((city) => city.nationId === nation.id);
const nationGenerals = generalRows.filter((general) => general.nationId === nation.id); const nationGenerals = generalRows.filter((general) => general.nationId === nation.id);
+6 -7
View File
@@ -143,9 +143,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
const nationGenerals = generalsByNation.get(nation.id) ?? []; const nationGenerals = generalsByNation.get(nation.id) ?? [];
const nationCities = citiesByNation.get(nation.id) ?? []; const nationCities = citiesByNation.get(nation.id) ?? [];
const officers = Array.from({ length: 8 }, (_, index) => 12 - index).map((officerLevel) => { const officers = Array.from({ length: 8 }, (_, index) => 12 - index).map((officerLevel) => {
const general = nationGenerals const general = nationGenerals.filter((candidate) => candidate.officerLevel === officerLevel).at(-1);
.filter((candidate) => candidate.officerLevel === officerLevel)
.at(-1);
return { return {
officerLevel, officerLevel,
general: general general: general
@@ -178,7 +176,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
}, },
power: readMetaNumber(nation.meta, 'power'), power: readMetaNumber(nation.meta, 'power'),
capitalCityId: nation.capitalCityId ?? 0, capitalCityId: nation.capitalCityId ?? 0,
generalCount: nationGenerals.length, generalCount: readMetaNumber(nation.meta, 'gennum', nationGenerals.length),
cityCount: nationCities.length, cityCount: nationCities.length,
officers, officers,
ambassadorNames: secretPermissions ambassadorNames: secretPermissions
@@ -204,8 +202,8 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
}); });
}); });
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional()) export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional()).query(
.query(async ({ ctx, input }) => { async ({ ctx, input }) => {
await getMyGeneral(ctx); await getMyGeneral(ctx);
const sort = input?.sort ?? 9; const sort = input?.sort ?? 9;
const [generals, nations, accessLogs, worldState] = await Promise.all([ const [generals, nations, accessLogs, worldState] = await Promise.all([
@@ -365,4 +363,5 @@ export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: z
}); });
return { sort, generals: rows }; return { sort, generals: rows };
}); }
);
+23 -11
View File
@@ -7,10 +7,7 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
import type { GameApiContext } from '../../context.js'; import type { GameApiContext } from '../../context.js';
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js'; import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
import { import { generalAccessEndpointWeights, recordGeneralAccessWeight } from '../../services/generalAccess.js';
generalAccessEndpointWeights,
recordGeneralAccessWeight,
} from '../../services/generalAccess.js';
import { authedProcedure, router } from '../../trpc.js'; import { authedProcedure, router } from '../../trpc.js';
import { getMyGeneral } from '../shared/general.js'; import { getMyGeneral } from '../shared/general.js';
@@ -72,6 +69,7 @@ const parseYearbookNations = (value: unknown): YearbookNation[] => {
const resolveArchiveTarget = ( const resolveArchiveTarget = (
worldMeta: unknown, worldMeta: unknown,
profileId: string,
profileName: string, profileName: string,
requestedServerId?: string requestedServerId?: string
): { archiveKey: string; legacyAlias: string | null; isCurrentProfile: boolean } => { ): { archiveKey: string; legacyAlias: string | null; isCurrentProfile: boolean } => {
@@ -81,7 +79,12 @@ const resolveArchiveTarget = (
const isCurrentProfile = requested === profileName || requested === canonicalServerId; const isCurrentProfile = requested === profileName || requested === canonicalServerId;
return { return {
archiveKey: isCurrentProfile ? canonicalServerId : requested, archiveKey: isCurrentProfile ? canonicalServerId : requested,
legacyAlias: isCurrentProfile && canonicalServerId !== profileName ? profileName : null, legacyAlias:
isCurrentProfile && canonicalServerId !== profileName
? profileName
: isCurrentProfile && profileId !== canonicalServerId
? profileId
: null,
isCurrentProfile, isCurrentProfile,
}; };
}; };
@@ -263,7 +266,7 @@ export const yearbookRouter = router({
message: 'World state is not initialized.', message: 'World state is not initialized.',
}); });
} }
const target = resolveArchiveTarget(worldState.meta, ctx.profile.name, input?.serverID); const target = resolveArchiveTarget(worldState.meta, ctx.profile.id, ctx.profile.name, input?.serverID);
const findRange = async (profileName: string) => const findRange = async (profileName: string) =>
Promise.all([ Promise.all([
@@ -278,10 +281,19 @@ export const yearbookRouter = router({
orderBy: [{ year: 'desc' as const }, { month: 'desc' as const }], orderBy: [{ year: 'desc' as const }, { month: 'desc' as const }],
}), }),
]); ]);
let [firstRow, lastRow] = await findRange(target.archiveKey); const ranges = await Promise.all(
if ((!firstRow || !lastRow) && target.legacyAlias) { [target.archiveKey, target.legacyAlias]
[firstRow, lastRow] = await findRange(target.legacyAlias); .filter((value): value is string => Boolean(value))
} .map(findRange)
);
const firstRow = ranges
.map(([first]) => first)
.filter((row): row is NonNullable<typeof row> => Boolean(row))
.sort((a, b) => joinYearMonth(a.year, a.month) - joinYearMonth(b.year, b.month))[0];
const lastRow = ranges
.map(([, last]) => last)
.filter((row): row is NonNullable<typeof row> => Boolean(row))
.sort((a, b) => joinYearMonth(b.year, b.month) - joinYearMonth(a.year, a.month))[0];
if (!target.isCurrentProfile && (!firstRow || !lastRow)) { if (!target.isCurrentProfile && (!firstRow || !lastRow)) {
throw new TRPCError({ code: 'NOT_FOUND', message: '연감 범위를 찾을 수 없습니다.' }); throw new TRPCError({ code: 'NOT_FOUND', message: '연감 범위를 찾을 수 없습니다.' });
@@ -314,7 +326,7 @@ export const yearbookRouter = router({
if (!worldState) { if (!worldState) {
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' }); throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
} }
const target = resolveArchiveTarget(worldState.meta, ctx.profile.name, input.serverID); const target = resolveArchiveTarget(worldState.meta, ctx.profile.id, ctx.profile.name, input.serverID);
const shouldRecordAfterHashCheck = target.isCurrentProfile && Boolean(input.hash); const shouldRecordAfterHashCheck = target.isCurrentProfile && Boolean(input.hash);
if (target.isCurrentProfile && !shouldRecordAfterHashCheck) { if (target.isCurrentProfile && !shouldRecordAfterHashCheck) {
await recordHistoryAccess(ctx); await recordHistoryAccess(ctx);
@@ -79,7 +79,7 @@ const createContext = (options: {
nationMeta?: Record<string, unknown>; nationMeta?: Record<string, unknown>;
requestCommand?: ReturnType<typeof vi.fn>; requestCommand?: ReturnType<typeof vi.fn>;
accessToken?: string; accessToken?: string;
logs?: Array<{ id: number; text: string }>; logs?: Array<{ id: number; text: string; year?: number; month?: number; createdAt?: Date }>;
}) => { }) => {
const me = options.me === undefined ? buildGeneral() : options.me; const me = options.me === undefined ? buildGeneral() : options.me;
const targets = options.targets ?? (me ? [me] : []); const targets = options.targets ?? (me ? [me] : []);
@@ -119,12 +119,24 @@ const createContext = (options: {
}, },
logEntry: { logEntry: {
groupBy: vi.fn(async () => []), groupBy: vi.fn(async () => []),
findMany: vi.fn(async (query?: { where?: { id?: { lt?: number } }; take?: number }) => { findMany: vi.fn(
const source = options.logs ?? [{ id: 1, text: '기록' }]; async (query?: {
const beforeId = query?.where?.id?.lt; where?: { id?: { lt?: number } };
const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source; take?: number;
return query?.take ? filtered.slice(0, query.take) : filtered; select?: { id?: boolean; text?: boolean };
}), }) => {
const source = (options.logs ?? [{ id: 1, text: '기록' }]).map((entry) => ({
year: 185,
month: 1,
createdAt: now,
...entry,
}));
const beforeId = query?.where?.id?.lt;
const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source;
const selected = query?.select ? filtered.map(({ id, text }) => ({ id, text })) : filtered;
return query?.take ? selected.slice(0, query.take) : selected;
}
),
}, },
}; };
const redisClient = { get: async () => null, set: async () => null }; const redisClient = { get: async () => null, set: async () => null };
@@ -405,6 +417,14 @@ describe('battle-center general and user permissions', () => {
}); });
await expect(appRouter.createCaller(tenured.context).nation.getBattleCenter()).resolves.toMatchObject({ await expect(appRouter.createCaller(tenured.context).nation.getBattleCenter()).resolves.toMatchObject({
me: { id: 7, permissionLevel: 1 }, me: { id: 7, permissionLevel: 1 },
generals: [
{
id: 7,
picture: 'default.jpg',
imageServer: 0,
battleStats: { kills: 0, deaths: 0, fire: 0, killCrew: 0, deathCrew: 0, dex: [0, 0, 0, 0, 0] },
},
],
}); });
const auditor = createContext({ const auditor = createContext({
@@ -430,6 +450,7 @@ describe('battle-center general and user permissions', () => {
await expect(member.nation.getGeneralLog({ generalId: me.id, type: 'generalAction' })).resolves.toMatchObject({ await expect(member.nation.getGeneralLog({ generalId: me.id, type: 'generalAction' })).resolves.toMatchObject({
generalId: me.id, generalId: me.id,
logs: [{ id: 1, year: 185, month: 1, createdAt: '2026-01-01 00:00:00' }],
}); });
await expect( await expect(
member.nation.getGeneralLog({ generalId: otherUser.id, type: 'generalAction' }) member.nation.getGeneralLog({ generalId: otherUser.id, type: 'generalAction' })
@@ -52,12 +52,27 @@ const archiveRows = [
year: 219, year: 219,
month: 12, month: 12,
map: { year: 219, month: 12, startYear: 190, cityList: [], nationList: [] }, map: { year: 219, month: 12, startYear: 190, cityList: [], nationList: [] },
nations: [{ id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] }], nations: [
{ id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] },
],
globalHistory: ['저장된 현재 기수 과거 기록'], globalHistory: ['저장된 현재 기수 과거 기록'],
globalAction: ['저장된 현재 기수 과거 행동'], globalAction: ['저장된 현재 기수 과거 행동'],
hash: 'current-archive', hash: 'current-archive',
createdAt: new Date('2026-07-31T00:00:00.000Z'), createdAt: new Date('2026-07-31T00:00:00.000Z'),
}, },
{
id: 4,
profileName: profile.id,
sourceId: 201,
year: 219,
month: 11,
map: { year: 219, month: 11, startYear: 190, cityList: [], nationList: [] },
nations: [],
globalHistory: ['레거시 프로필 별칭 기록'],
globalAction: [],
hash: 'legacy-profile-alias',
createdAt: new Date('2026-07-30T00:00:00.000Z'),
},
]; ];
const authFor = (userId: string): GameSessionTokenPayload => ({ const authFor = (userId: string): GameSessionTokenPayload => ({
@@ -75,14 +90,21 @@ const authFor = (userId: string): GameSessionTokenPayload => ({
sanctions: {}, sanctions: {},
}); });
const buildContext = (auth: GameSessionTokenPayload | null, options: { hasGeneral?: boolean } = {}): GameApiContext => { const buildContext = (
auth: GameSessionTokenPayload | null,
options: { hasGeneral?: boolean; worldMeta?: unknown } = {}
): GameApiContext => {
const db = { const db = {
general: { general: {
findFirst: async ({ where }: { where: { userId: string } }) => findFirst: async ({ where }: { where: { userId: string } }) =>
options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId }, options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
}, },
worldState: { worldState: {
findFirst: async () => ({ currentYear: 220, currentMonth: 1, meta: { serverId: currentServerId } }), findFirst: async () => ({
currentYear: 220,
currentMonth: 1,
meta: options.worldMeta ?? { serverId: currentServerId },
}),
}, },
yearbookHistory: { yearbookHistory: {
findFirst: async (args: { findFirst: async (args: {
@@ -198,6 +220,16 @@ describe('historical yearbook access from dynasty', () => {
}); });
}); });
it('reads imported history under the short profile ID when world metadata has no server ID', async () => {
const caller = appRouter.createCaller(buildContext(authFor('owner-a'), { worldMeta: {} }));
await expect(caller.yearbook.getRange()).resolves.toEqual({
firstYearMonth: 219 * 12 + 10,
lastYearMonth: 219 * 12 + 10,
currentYearMonth: 220 * 12,
});
});
it('uses stored logs for a past month of the current generation', async () => { it('uses stored logs for a past month of the current generation', async () => {
const caller = appRouter.createCaller(buildContext(authFor('owner-a'))); const caller = appRouter.createCaller(buildContext(authFor('owner-a')));