feat: make general creation daemon-atomic
This commit is contained in:
@@ -9,6 +9,7 @@ export interface UserSanctions {
|
||||
notes?: string;
|
||||
profileIconResetAt?: string;
|
||||
serverRestrictions?: Record<string, UserServerRestriction>;
|
||||
legacyPenalty?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UserServerRestriction {
|
||||
@@ -23,6 +24,9 @@ export interface GatewayUserInfo {
|
||||
username: string;
|
||||
displayName: string;
|
||||
roles: string[];
|
||||
picture?: string;
|
||||
imageServer?: number;
|
||||
canUseGeneralPicture?: boolean;
|
||||
createdAt?: string;
|
||||
legacyMemberNo?: number;
|
||||
}
|
||||
@@ -58,7 +62,7 @@ export const encryptGameSessionToken = (payload: GameSessionTokenPayload, secret
|
||||
return `${toBase64Url(iv)}.${toBase64Url(ciphertext)}.${toBase64Url(tag)}`;
|
||||
};
|
||||
|
||||
const parsePayload = (value: unknown): GameSessionTokenPayload | null => {
|
||||
export const parseGameSessionTokenPayload = (value: unknown): GameSessionTokenPayload | null => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null;
|
||||
}
|
||||
@@ -84,8 +88,10 @@ const parsePayload = (value: unknown): GameSessionTokenPayload | null => {
|
||||
typeof user.username !== 'string' ||
|
||||
typeof user.displayName !== 'string' ||
|
||||
!Array.isArray(user.roles) ||
|
||||
(user.legacyMemberNo !== undefined &&
|
||||
(!Number.isSafeInteger(user.legacyMemberNo) || user.legacyMemberNo <= 0))
|
||||
(user.picture !== undefined && typeof user.picture !== 'string') ||
|
||||
(user.imageServer !== undefined && (!Number.isSafeInteger(user.imageServer) || user.imageServer < 0)) ||
|
||||
(user.canUseGeneralPicture !== undefined && typeof user.canUseGeneralPicture !== 'boolean') ||
|
||||
(user.legacyMemberNo !== undefined && (!Number.isSafeInteger(user.legacyMemberNo) || user.legacyMemberNo <= 0))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -123,7 +129,7 @@ export const decryptGameSessionToken = (token: string, secret: string): GameSess
|
||||
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
||||
return parsePayload(JSON.parse(plaintext));
|
||||
return parseGameSessionTokenPayload(JSON.parse(plaintext));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -204,6 +204,28 @@ export type TurnDaemonCommand =
|
||||
specialWar?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: 'joinCreateGeneral';
|
||||
requestId?: string;
|
||||
userId: string;
|
||||
ownerDisplayName: string;
|
||||
seedOwnerIdentity: string | number;
|
||||
name: string;
|
||||
leadership: number;
|
||||
strength: number;
|
||||
intel: number;
|
||||
pic: boolean;
|
||||
character: string;
|
||||
profileId: string;
|
||||
ownerPicture?: string;
|
||||
ownerImageServer?: number;
|
||||
ownerCanUsePicture?: boolean;
|
||||
ownerLegacyPenalty?: Record<string, unknown>;
|
||||
inheritSpecial?: string;
|
||||
inheritTurntimeZone?: number;
|
||||
inheritCity?: number;
|
||||
inheritBonusStat?: [number, number, number];
|
||||
}
|
||||
| {
|
||||
type: 'selectPoolCreate';
|
||||
requestId?: string;
|
||||
@@ -476,6 +498,17 @@ export type TurnDaemonCommandResult =
|
||||
generalId: number;
|
||||
reason: string;
|
||||
}
|
||||
| {
|
||||
type: 'joinCreateGeneral';
|
||||
ok: true;
|
||||
generalId: number;
|
||||
}
|
||||
| {
|
||||
type: 'joinCreateGeneral';
|
||||
ok: false;
|
||||
code: 'BAD_REQUEST' | 'FORBIDDEN' | 'PRECONDITION_FAILED' | 'CONFLICT' | 'INTERNAL_SERVER_ERROR';
|
||||
reason: string;
|
||||
}
|
||||
| {
|
||||
type: 'selectPoolCreate';
|
||||
ok: true;
|
||||
|
||||
@@ -204,6 +204,7 @@ export interface TurnEngineGeneralUpdateInput {
|
||||
specialCode: string;
|
||||
special2Code: string;
|
||||
lastTurn: InputJsonValue;
|
||||
penalty: InputJsonValue;
|
||||
meta: InputJsonValue;
|
||||
turnTime: Date;
|
||||
recentWarTime: Date | null;
|
||||
|
||||
@@ -16,6 +16,11 @@ export const PERSONALITY_TRAIT_KEYS = [
|
||||
|
||||
export type PersonalityTraitKey = (typeof PERSONALITY_TRAIT_KEYS)[number];
|
||||
|
||||
// Ref GameConst::$availablePersonality에는 은둔이 포함되지 않는다.
|
||||
export const JOIN_PERSONALITY_TRAIT_KEYS = PERSONALITY_TRAIT_KEYS.filter(
|
||||
(key): key is Exclude<PersonalityTraitKey, 'che_은둔'> => key !== 'che_은둔'
|
||||
);
|
||||
|
||||
export type PersonalityTraitModule = TraitModule;
|
||||
|
||||
export type PersonalityTraitImporter = () => Promise<TraitModuleExport>;
|
||||
|
||||
@@ -179,11 +179,14 @@ const readNameParts = (value: unknown, fallback: readonly string[]): string[] =>
|
||||
return result.length > 0 ? result : [...fallback];
|
||||
};
|
||||
|
||||
export const buildAuctionAlias = (
|
||||
generalId: number,
|
||||
export const buildAuctionAliasPool = (
|
||||
hiddenSeed: string | number,
|
||||
configConst: Record<string, unknown> = {}
|
||||
): string => {
|
||||
): string[] => {
|
||||
const persistedPool = readNameParts(configConst.obfuscatedNamePool, []);
|
||||
if (persistedPool.length > 0) {
|
||||
return persistedPool;
|
||||
}
|
||||
const firstNames = readNameParts(configConst.randGenFirstName, LEGACY_RANDOM_GENERAL_FIRST_NAMES);
|
||||
const middleNames = readNameParts(configConst.randGenMiddleName, ['']);
|
||||
const lastNames = readNameParts(configConst.randGenLastName, LEGACY_RANDOM_GENERAL_LAST_NAMES);
|
||||
@@ -195,7 +198,15 @@ export const buildAuctionAlias = (
|
||||
}
|
||||
}
|
||||
}
|
||||
const shuffled = new RandUtil(new LiteHashDRBG(simpleSerialize(hiddenSeed, 'obfuscatedNamePool'))).shuffle(pool);
|
||||
return new RandUtil(new LiteHashDRBG(simpleSerialize(hiddenSeed, 'obfuscatedNamePool'))).shuffle(pool);
|
||||
};
|
||||
|
||||
export const buildAuctionAlias = (
|
||||
generalId: number,
|
||||
hiddenSeed: string | number,
|
||||
configConst: Record<string, unknown> = {}
|
||||
): string => {
|
||||
const shuffled = buildAuctionAliasPool(hiddenSeed, configConst);
|
||||
const normalizedId = Math.max(0, Math.floor(generalId));
|
||||
const duplicateIndex = Math.floor(normalizedId / shuffled.length);
|
||||
const name = shuffled[normalizedId % shuffled.length] ?? `익명${normalizedId}`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildAuctionAlias } from '../src/auction/alias.js';
|
||||
import { buildAuctionAlias, buildAuctionAliasPool } from '../src/auction/alias.js';
|
||||
|
||||
describe('buildAuctionAlias', () => {
|
||||
it('returns a stable alias for the same world seed and general id', () => {
|
||||
@@ -22,4 +22,18 @@ describe('buildAuctionAlias', () => {
|
||||
expect(buildAuctionAlias(1, 'seed', config)).toMatch(/^청운(객|상)$/);
|
||||
expect(buildAuctionAlias(2, 'seed', config)).toMatch(/^청운(객|상)1$/);
|
||||
});
|
||||
|
||||
it('reuses the world-persisted alias pool after general creation changes RNG inputs', () => {
|
||||
const config = {
|
||||
obfuscatedNamePool: ['고정별호A', '고정별호B'],
|
||||
randGenFirstName: ['변경'],
|
||||
randGenMiddleName: ['된'],
|
||||
randGenLastName: ['이름'],
|
||||
};
|
||||
|
||||
expect(buildAuctionAliasPool('new-seed', config)).toEqual(['고정별호A', '고정별호B']);
|
||||
expect(buildAuctionAlias(0, 'new-seed', config)).toBe('고정별호A');
|
||||
expect(buildAuctionAlias(1, 'new-seed', config)).toBe('고정별호B');
|
||||
expect(buildAuctionAlias(2, 'new-seed', config)).toBe('고정별호A1');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user