merge: 최신 main을 자동 외교 분리 브랜치에 통합한다
This commit is contained in:
@@ -416,7 +416,6 @@ export const voteRouter = router({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const general = await getMyGeneral(ctx);
|
const general = await getMyGeneral(ctx);
|
||||||
const openerName = ctx.auth?.user.username ?? general.name;
|
|
||||||
const options = normalizeOptions(input.options);
|
const options = normalizeOptions(input.options);
|
||||||
if (options.length === 0) {
|
if (options.length === 0) {
|
||||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '항목이 없습니다.' });
|
throw new TRPCError({ code: 'BAD_REQUEST', message: '항목이 없습니다.' });
|
||||||
@@ -468,7 +467,7 @@ export const voteRouter = router({
|
|||||||
${multipleOptions},
|
${multipleOptions},
|
||||||
${input.revealMode},
|
${input.revealMode},
|
||||||
${general.id},
|
${general.id},
|
||||||
${openerName},
|
${general.name},
|
||||||
${gameTime.now},
|
${gameTime.now},
|
||||||
${gameTime.tick === null ? null : BigInt(gameTime.tick)},
|
${gameTime.tick === null ? null : BigInt(gameTime.tick)},
|
||||||
${endAt},
|
${endAt},
|
||||||
|
|||||||
@@ -287,12 +287,16 @@ integration('generic general creation through the durable turn daemon', () => {
|
|||||||
accountIconUpdatedAt: '2026-07-30T00:00:00.000Z',
|
accountIconUpdatedAt: '2026-07-30T00:00:00.000Z',
|
||||||
});
|
});
|
||||||
const createdAccess = await db.generalAccessLog.findUniqueOrThrow({ where: { generalId: created.id } });
|
const createdAccess = await db.generalAccessLog.findUniqueOrThrow({ where: { generalId: created.id } });
|
||||||
|
const acceptedEvent = await db.inputEvent.findUniqueOrThrow({
|
||||||
|
where: { requestId: `join-create:${userId}:${clientRequestId}` },
|
||||||
|
});
|
||||||
if (!createdAccess.lastRefresh) {
|
if (!createdAccess.lastRefresh) {
|
||||||
throw new Error('created general must have an initial access timestamp');
|
throw new Error('created general must have an initial access timestamp');
|
||||||
}
|
}
|
||||||
|
expect(createdAccess.lastRefresh).toEqual(acceptedEvent.createdAt);
|
||||||
expect(
|
expect(
|
||||||
new Date((created.meta as Record<string, unknown>).prestart_delete_after as string).getTime() -
|
new Date((created.meta as Record<string, unknown>).prestart_delete_after as string).getTime() -
|
||||||
createdAccess.lastRefresh.getTime()
|
acceptedEvent.createdAt.getTime()
|
||||||
).toBe(2 * 5 * 60 * 1_000);
|
).toBe(2 * 5 * 60 * 1_000);
|
||||||
expect(runtime!.world.getGeneralById(created.id)).toMatchObject({
|
expect(runtime!.world.getGeneralById(created.id)).toMatchObject({
|
||||||
id: created.id,
|
id: created.id,
|
||||||
@@ -354,7 +358,7 @@ integration('generic general creation through the durable turn daemon', () => {
|
|||||||
attempts: 1,
|
attempts: 1,
|
||||||
actorUserId: userId,
|
actorUserId: userId,
|
||||||
});
|
});
|
||||||
expect(access.lastRefresh?.getTime()).toBe(runtime!.world.getGameNow(event.createdAt).getTime());
|
expect(access.lastRefresh?.getTime()).toBe(event.createdAt.getTime());
|
||||||
const turnGridOffsetSeconds =
|
const turnGridOffsetSeconds =
|
||||||
((created.turnTime.getTime() - runtime!.world.getState().lastTurnTime.getTime()) / 1000 + 300) % 300;
|
((created.turnTime.getTime() - runtime!.world.getState().lastTurnTime.getTime()) / 1000 + 300) % 300;
|
||||||
expect(turnGridOffsetSeconds).toBeGreaterThanOrEqual(35);
|
expect(turnGridOffsetSeconds).toBeGreaterThanOrEqual(35);
|
||||||
|
|||||||
@@ -257,12 +257,17 @@ integration('scenario 903 select pool through the durable turn daemon', () => {
|
|||||||
const initial = await db.general.findFirstOrThrow({ where: { userId } });
|
const initial = await db.general.findFirstOrThrow({ where: { userId } });
|
||||||
const initialRuntime = runtime!.world.getGeneralById(initial.id);
|
const initialRuntime = runtime!.world.getGeneralById(initial.id);
|
||||||
const initialAccess = await db.generalAccessLog.findUniqueOrThrow({ where: { generalId: initial.id } });
|
const initialAccess = await db.generalAccessLog.findUniqueOrThrow({ where: { generalId: initial.id } });
|
||||||
|
const acceptedEvent = await db.inputEvent.findFirstOrThrow({
|
||||||
|
where: { actorUserId: userId, eventType: 'selectPoolCreate', status: 'SUCCEEDED' },
|
||||||
|
orderBy: { sequence: 'desc' },
|
||||||
|
});
|
||||||
if (!initialAccess.lastRefresh) {
|
if (!initialAccess.lastRefresh) {
|
||||||
throw new Error('selected general must have an initial access timestamp');
|
throw new Error('selected general must have an initial access timestamp');
|
||||||
}
|
}
|
||||||
|
expect(initialAccess.lastRefresh).toEqual(acceptedEvent.createdAt);
|
||||||
expect(
|
expect(
|
||||||
new Date((initial.meta as Record<string, unknown>).prestart_delete_after as string).getTime() -
|
new Date((initial.meta as Record<string, unknown>).prestart_delete_after as string).getTime() -
|
||||||
initialAccess.lastRefresh.getTime()
|
acceptedEvent.createdAt.getTime()
|
||||||
).toBe(2 * 5 * 60 * 1_000);
|
).toBe(2 * 5 * 60 * 1_000);
|
||||||
expect(initialRuntime).toMatchObject({
|
expect(initialRuntime).toMatchObject({
|
||||||
id: initial.id,
|
id: initial.id,
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ describe('vote router actor and permission boundaries', () => {
|
|||||||
expect(fixture.redisPublish).not.toHaveBeenCalled();
|
expect(fixture.redisPublish).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('publishes a global front-status projection after creating a survey', async () => {
|
it('stores the authenticated general name and publishes a global projection after creating a survey', async () => {
|
||||||
const auth = buildAuth(['admin.survey.open']);
|
const auth = buildAuth(['admin.survey.open']);
|
||||||
auth.user.username = 'admin-account';
|
auth.user.username = 'admin-account';
|
||||||
auth.user.displayName = '관리자 표시명';
|
auth.user.displayName = '관리자 표시명';
|
||||||
@@ -285,8 +285,9 @@ describe('vote router actor and permission boundaries', () => {
|
|||||||
const insert = fixture.queryRaw.mock.calls
|
const insert = fixture.queryRaw.mock.calls
|
||||||
.map(([query]) => query)
|
.map(([query]) => query)
|
||||||
.find((query) => sqlText(query).includes('INSERT INTO vote_poll'));
|
.find((query) => sqlText(query).includes('INSERT INTO vote_poll'));
|
||||||
expect(insert?.values).toContain('admin-account');
|
expect(insert?.values).toContain('관리자 장수');
|
||||||
expect(insert?.values).not.toContain('관리자 장수');
|
expect(insert?.values).not.toContain('admin-account');
|
||||||
|
expect(insert?.values).not.toContain('관리자 표시명');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports the current world develcost as the legacy five-times survey reward', async () => {
|
it('reports the current world develcost as the legacy five-times survey reward', async () => {
|
||||||
|
|||||||
@@ -522,8 +522,9 @@ export const createGeneralFromJoin = async (options: {
|
|||||||
worldState: WorldStateRow;
|
worldState: WorldStateRow;
|
||||||
input: JoinCreateGeneralInput;
|
input: JoinCreateGeneralInput;
|
||||||
acceptedAt: Date;
|
acceptedAt: Date;
|
||||||
|
operationalAcceptedAt: Date;
|
||||||
}): Promise<{ ok: true; generalId: number }> => {
|
}): Promise<{ ok: true; generalId: number }> => {
|
||||||
const { db, world, worldState, input, acceptedAt } = options;
|
const { db, world, worldState, input, acceptedAt, operationalAcceptedAt } = options;
|
||||||
await lockJoinMutation(db, input.userId);
|
await lockJoinMutation(db, input.userId);
|
||||||
await assertGeneralIdSnapshotMatches(db, world);
|
await assertGeneralIdSnapshotMatches(db, world);
|
||||||
|
|
||||||
@@ -734,7 +735,10 @@ export const createGeneralFromJoin = async (options: {
|
|||||||
const nextInheritancePoint = currentInheritancePoint - inheritRequiredPoint;
|
const nextInheritancePoint = currentInheritancePoint - inheritRequiredPoint;
|
||||||
const restInheritanceBonus = await resolveRestInheritanceBonus(db, worldState, input.userId);
|
const restInheritanceBonus = await resolveRestInheritanceBonus(db, worldState, input.userId);
|
||||||
const finalInheritancePoint = nextInheritancePoint + restInheritanceBonus;
|
const finalInheritancePoint = nextInheritancePoint + restInheritanceBonus;
|
||||||
const prestartDeleteAfter = buildPrestartDeleteAfter(acceptedAt, worldState.tickSeconds, config);
|
// Ref의 가오픈 삭제 대기는 정지된 게임 clock이 아니라 실제 요청 접수 시각부터 흐른다.
|
||||||
|
// 미래 정식 오픈에 clock을 고정한 PREOPEN에서도 사용자가 가오픈 중 두 턴을 기다리면
|
||||||
|
// 삭제할 수 있어야 하므로 RNG/턴 배치용 acceptedAt과 이 벽시계 경계를 분리한다.
|
||||||
|
const prestartDeleteAfter = buildPrestartDeleteAfter(operationalAcceptedAt, worldState.tickSeconds, config);
|
||||||
const general: TurnGeneral = {
|
const general: TurnGeneral = {
|
||||||
id: generalId,
|
id: generalId,
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
@@ -839,11 +843,11 @@ export const createGeneralFromJoin = async (options: {
|
|||||||
});
|
});
|
||||||
await db.generalAccessLog.upsert({
|
await db.generalAccessLog.upsert({
|
||||||
where: { generalId },
|
where: { generalId },
|
||||||
update: { userId: input.userId, lastRefresh: acceptedAt },
|
update: { userId: input.userId, lastRefresh: operationalAcceptedAt },
|
||||||
create: {
|
create: {
|
||||||
generalId,
|
generalId,
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
lastRefresh: acceptedAt,
|
lastRefresh: operationalAcceptedAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (inheritRequiredPoint > 0) {
|
if (inheritRequiredPoint > 0) {
|
||||||
|
|||||||
@@ -690,6 +690,7 @@ export const createGeneralFromSelectionPool = async (options: {
|
|||||||
uniqueName: string;
|
uniqueName: string;
|
||||||
personality: string;
|
personality: string;
|
||||||
now?: Date;
|
now?: Date;
|
||||||
|
operationalAcceptedAt: Date;
|
||||||
seedOwnerIdentity?: string | number;
|
seedOwnerIdentity?: string | number;
|
||||||
ownerPicture?: string;
|
ownerPicture?: string;
|
||||||
ownerImageServer?: number;
|
ownerImageServer?: number;
|
||||||
@@ -754,7 +755,7 @@ export const createGeneralFromSelectionPool = async (options: {
|
|||||||
const nextChangeAt = new Date(
|
const nextChangeAt = new Date(
|
||||||
now.getTime() + resolveTurnTermMinutes(worldState) * RESELECTION_TURN_MULTIPLIER * 60_000
|
now.getTime() + resolveTurnTermMinutes(worldState) * RESELECTION_TURN_MULTIPLIER * 60_000
|
||||||
);
|
);
|
||||||
const prestartDeleteAfter = buildPrestartDeleteAfter(now, worldState.tickSeconds, config);
|
const prestartDeleteAfter = buildPrestartDeleteAfter(options.operationalAcceptedAt, worldState.tickSeconds, config);
|
||||||
const showImgLevel = asNumber(config.showImgLevel, 0);
|
const showImgLevel = asNumber(config.showImgLevel, 0);
|
||||||
const useOwnerPicture =
|
const useOwnerPicture =
|
||||||
showImgLevel >= 1 && typeof options.ownerPicture === 'string' && options.ownerPicture !== 'default.jpg';
|
showImgLevel >= 1 && typeof options.ownerPicture === 'string' && options.ownerPicture !== 'default.jpg';
|
||||||
@@ -893,8 +894,8 @@ export const createGeneralFromSelectionPool = async (options: {
|
|||||||
}
|
}
|
||||||
await db.generalAccessLog.upsert({
|
await db.generalAccessLog.upsert({
|
||||||
where: { generalId },
|
where: { generalId },
|
||||||
update: { userId, lastRefresh: now },
|
update: { userId, lastRefresh: options.operationalAcceptedAt },
|
||||||
create: { generalId, userId, lastRefresh: now },
|
create: { generalId, userId, lastRefresh: options.operationalAcceptedAt },
|
||||||
});
|
});
|
||||||
await clearUnusedReservations(db, userId, now, nowTick);
|
await clearUnusedReservations(db, userId, now, nowTick);
|
||||||
await synchronizeSelectionPoolWorld(db, world);
|
await synchronizeSelectionPoolWorld(db, world);
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ async function handleJoinCreateGeneral(
|
|||||||
...(command.inheritBonusStat !== undefined ? { inheritBonusStat: command.inheritBonusStat } : {}),
|
...(command.inheritBonusStat !== undefined ? { inheritBonusStat: command.inheritBonusStat } : {}),
|
||||||
},
|
},
|
||||||
acceptedAt,
|
acceptedAt,
|
||||||
|
operationalAcceptedAt,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -366,7 +367,13 @@ async function handleSelectPoolCreate(
|
|||||||
if (!worldState) {
|
if (!worldState) {
|
||||||
throw new Error('Selection-pool world state is missing.');
|
throw new Error('Selection-pool world state is missing.');
|
||||||
}
|
}
|
||||||
const acceptedAt = await resolveSelectionCommandAcceptedAt(db, ctx.world, command);
|
const operationalAcceptedAt = await resolveCommandAcceptedAt(db, command);
|
||||||
|
const acceptedAt =
|
||||||
|
command.acceptedGameTick !== undefined
|
||||||
|
? ctx.world.gameTickToDate(command.acceptedGameTick)
|
||||||
|
: command.acceptedGameAt !== undefined
|
||||||
|
? new Date(command.acceptedGameAt)
|
||||||
|
: ctx.world.getGameNow(operationalAcceptedAt);
|
||||||
try {
|
try {
|
||||||
return {
|
return {
|
||||||
type: 'selectPoolCreate',
|
type: 'selectPoolCreate',
|
||||||
@@ -383,6 +390,7 @@ async function handleSelectPoolCreate(
|
|||||||
...(command.ownerImageServer !== undefined ? { ownerImageServer: command.ownerImageServer } : {}),
|
...(command.ownerImageServer !== undefined ? { ownerImageServer: command.ownerImageServer } : {}),
|
||||||
...(command.ownerIconRevision ? { ownerIconRevision: command.ownerIconRevision } : {}),
|
...(command.ownerIconRevision ? { ownerIconRevision: command.ownerIconRevision } : {}),
|
||||||
now: acceptedAt,
|
now: acceptedAt,
|
||||||
|
operationalAcceptedAt,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user