feat: synchronize account icons across game profiles
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { TurnDaemonCommand, TurnDaemonCommandType, TurnDaemonCommandByType } from '@sammo-ts/common';
|
||||
import { isCanonicalIsoTimestamp } from '@sammo-ts/common';
|
||||
|
||||
export type TurnDaemonCommandEnvelope = {
|
||||
requestId: string;
|
||||
@@ -252,6 +253,17 @@ const zPatchGeneral = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const zAdjustGeneralIcon = z
|
||||
.object({
|
||||
type: z.literal('adjustGeneralIcon'),
|
||||
requestId: z.string().min(1).optional(),
|
||||
userId: z.string().min(1),
|
||||
picture: z.string().min(1),
|
||||
imageServer: z.number().int().nonnegative(),
|
||||
iconRevision: z.string().refine(isCanonicalIsoTimestamp),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const zJoinCreateGeneral = z
|
||||
.object({
|
||||
type: z.literal('joinCreateGeneral'),
|
||||
@@ -268,6 +280,7 @@ const zJoinCreateGeneral = z
|
||||
profileId: z.string().min(1),
|
||||
ownerPicture: z.string().optional(),
|
||||
ownerImageServer: zFiniteNumber.optional(),
|
||||
ownerIconRevision: z.string().refine(isCanonicalIsoTimestamp).optional(),
|
||||
ownerCanUsePicture: z.boolean().optional(),
|
||||
ownerLegacyPenalty: zRecord.optional(),
|
||||
inheritSpecial: z.string().optional(),
|
||||
@@ -566,6 +579,14 @@ const normalizePatchGeneral: CommandNormalizer<'patchGeneral'> = (envelope) => {
|
||||
return { ...command, requestId: envelope.requestId };
|
||||
};
|
||||
|
||||
const normalizeAdjustGeneralIcon: CommandNormalizer<'adjustGeneralIcon'> = (envelope) => {
|
||||
const command = parseWith(zAdjustGeneralIcon, envelope.command);
|
||||
if (!command || (command.requestId !== undefined && command.requestId !== envelope.requestId)) {
|
||||
return null;
|
||||
}
|
||||
return { ...command, requestId: envelope.requestId };
|
||||
};
|
||||
|
||||
const normalizeJoinCreateGeneral: CommandNormalizer<'joinCreateGeneral'> = (envelope) => {
|
||||
const command = parseWith(zJoinCreateGeneral, envelope.command);
|
||||
if (!command) {
|
||||
@@ -662,6 +683,7 @@ const normalizers: CommandNormalizerMap = {
|
||||
adjustGeneralMeta: normalizeAdjustGeneralMeta,
|
||||
tournamentMatchResult: normalizeTournamentMatchResult,
|
||||
patchGeneral: normalizePatchGeneral,
|
||||
adjustGeneralIcon: normalizeAdjustGeneralIcon,
|
||||
joinCreateGeneral: normalizeJoinCreateGeneral,
|
||||
npcPossessGeneral: normalizeNpcPossessGeneral,
|
||||
selectPoolCreate: normalizeSelectPoolCreate,
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface JoinCreateGeneralInput {
|
||||
profileId: string;
|
||||
ownerPicture?: string;
|
||||
ownerImageServer?: number;
|
||||
ownerIconRevision?: string;
|
||||
ownerCanUsePicture?: boolean;
|
||||
ownerLegacyPenalty?: Record<string, unknown>;
|
||||
inheritSpecial?: string;
|
||||
@@ -700,6 +701,12 @@ export const createGeneralFromJoin = async (options: {
|
||||
input.ownerPicture !== 'default.jpg';
|
||||
const picture = canUseOwnerPicture ? input.ownerPicture! : 'default.jpg';
|
||||
const imageServer = canUseOwnerPicture ? Math.max(0, Math.floor(input.ownerImageServer ?? 0)) : 0;
|
||||
const accountIconUpdatedAt =
|
||||
input.ownerIconRevision &&
|
||||
picture === input.ownerPicture &&
|
||||
imageServer === Math.max(0, Math.floor(input.ownerImageServer ?? 0))
|
||||
? input.ownerIconRevision
|
||||
: undefined;
|
||||
const nextInheritancePoint = currentInheritancePoint - inheritRequiredPoint;
|
||||
const restInheritanceBonus = await resolveRestInheritanceBonus(db, worldState, input.userId);
|
||||
const finalInheritancePoint = nextInheritancePoint + restInheritanceBonus;
|
||||
@@ -781,6 +788,7 @@ export const createGeneralFromJoin = async (options: {
|
||||
newvote: 0,
|
||||
inherit_spent_dyn: inheritRequiredPoint,
|
||||
prestart_delete_after: prestartDeleteAfter.toISOString(),
|
||||
...(accountIconUpdatedAt ? { accountIconUpdatedAt } : {}),
|
||||
},
|
||||
};
|
||||
if (!world.addGeneral(general)) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
TurnDaemonCommandResult,
|
||||
} from '../lifecycle/types.js';
|
||||
import { GamePrisma, type DatabaseClient } from '@sammo-ts/infra';
|
||||
import { asRecord, JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import { asRecord, isCanonicalIsoTimestamp, JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import {
|
||||
LogCategory,
|
||||
LogFormat,
|
||||
@@ -133,7 +133,7 @@ interface CommandHandlerContext {
|
||||
|
||||
const requireCommandDatabase = (ctx: CommandHandlerContext): DatabaseClient => {
|
||||
if (!ctx.commandDb) {
|
||||
throw new Error('ENGINE mutation transaction is required for selection-pool commands.');
|
||||
throw new Error('ENGINE mutation transaction is required for authenticated commands.');
|
||||
}
|
||||
return ctx.commandDb as unknown as DatabaseClient;
|
||||
};
|
||||
@@ -149,7 +149,8 @@ const resolveCommandAcceptedAt = async (
|
||||
| 'joinCreateGeneral'
|
||||
| 'npcPossessGeneral'
|
||||
| 'selectPoolCreate'
|
||||
| 'selectPoolReselect';
|
||||
| 'selectPoolReselect'
|
||||
| 'adjustGeneralIcon';
|
||||
}
|
||||
>
|
||||
): Promise<Date> => {
|
||||
@@ -228,6 +229,9 @@ async function handleJoinCreateGeneral(
|
||||
profileId: command.profileId,
|
||||
...(command.ownerPicture !== undefined ? { ownerPicture: command.ownerPicture } : {}),
|
||||
...(command.ownerImageServer !== undefined ? { ownerImageServer: command.ownerImageServer } : {}),
|
||||
...(command.ownerIconRevision !== undefined
|
||||
? { ownerIconRevision: command.ownerIconRevision }
|
||||
: {}),
|
||||
...(command.ownerCanUsePicture !== undefined
|
||||
? { ownerCanUsePicture: command.ownerCanUsePicture }
|
||||
: {}),
|
||||
@@ -702,6 +706,78 @@ async function handlePatchGeneral(
|
||||
return { type: 'patchGeneral', ok: true, generalId: command.generalId };
|
||||
}
|
||||
|
||||
async function handleAdjustGeneralIcon(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'adjustGeneralIcon' }>
|
||||
): Promise<TurnDaemonCommandResult> {
|
||||
const db = requireCommandDatabase(ctx);
|
||||
await resolveCommandAcceptedAt(db, command);
|
||||
const general = ctx.world
|
||||
.listGenerals()
|
||||
.find((candidate) => candidate.userId === command.userId && candidate.npcState === 0);
|
||||
if (!general) {
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: true,
|
||||
generalId: null,
|
||||
updated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const currentRevision = general.meta.accountIconUpdatedAt;
|
||||
if (currentRevision !== undefined) {
|
||||
if (typeof currentRevision !== 'string' || !isCanonicalIsoTimestamp(currentRevision)) {
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: false,
|
||||
code: 'PRECONDITION_FAILED',
|
||||
reason: '장수의 계정 아이콘 revision이 올바르지 않습니다.',
|
||||
};
|
||||
}
|
||||
const currentRevisionTime = new Date(currentRevision).getTime();
|
||||
const nextRevisionTime = new Date(command.iconRevision).getTime();
|
||||
if (nextRevisionTime < currentRevisionTime) {
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: false,
|
||||
code: 'CONFLICT',
|
||||
reason: '더 최신 계정 아이콘이 이미 적용되었습니다.',
|
||||
};
|
||||
}
|
||||
if (nextRevisionTime === currentRevisionTime) {
|
||||
if (general.picture === command.picture && general.imageServer === command.imageServer) {
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: true,
|
||||
generalId: general.id,
|
||||
updated: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: false,
|
||||
code: 'CONFLICT',
|
||||
reason: '같은 revision에 다른 계정 아이콘이 요청되었습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
ctx.world.updateGeneral(general.id, {
|
||||
picture: command.picture,
|
||||
imageServer: command.imageServer,
|
||||
meta: {
|
||||
...general.meta,
|
||||
accountIconUpdatedAt: command.iconRevision,
|
||||
},
|
||||
});
|
||||
return {
|
||||
type: 'adjustGeneralIcon',
|
||||
ok: true,
|
||||
generalId: general.id,
|
||||
updated: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function handleShiftSchedule(
|
||||
ctx: CommandHandlerContext,
|
||||
command: Extract<TurnDaemonCommand, { type: 'shiftSchedule' }>
|
||||
@@ -2296,6 +2372,8 @@ export const createTurnDaemonCommandHandler = (options: {
|
||||
handleTournamentMatchResult(ctx, command as Extract<TurnDaemonCommand, { type: 'tournamentMatchResult' }>),
|
||||
patchGeneral: (command) =>
|
||||
handlePatchGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'patchGeneral' }>),
|
||||
adjustGeneralIcon: (command) =>
|
||||
handleAdjustGeneralIcon(ctx, command as Extract<TurnDaemonCommand, { type: 'adjustGeneralIcon' }>),
|
||||
joinCreateGeneral: (command) =>
|
||||
handleJoinCreateGeneral(ctx, command as Extract<TurnDaemonCommand, { type: 'joinCreateGeneral' }>),
|
||||
npcPossessGeneral: (command) =>
|
||||
|
||||
Reference in New Issue
Block a user