///
declare const Buffer: any;
import {
AvatarSetting,
DancerGrade,
DeltaEntry,
FumenType,
MotionEntry,
MusicScoreEntry,
MusicUnlockGaugeEntry,
PlayInfo,
PlayOptionSetting,
ProfileDocument,
StageResult,
UnlockMusicEntry,
ViewFlagEntry,
} from './models';
export const PROFILE_SCHEMA_VERSION = 1;
export function nowMs(): number {
return Date.now();
}
export function nowS64(): bigint {
return BigInt(Date.now());
}
export function makeUserCode(refid: string): number {
let hash = 2166136261;
for (let i = 0; i < refid.length; ++i) {
hash ^= refid.charCodeAt(i);
hash = Math.imul(hash, 16777619) >>> 0;
}
return 100000000 + (hash % 900000000);
}
export function makeDancerId(userCode: number): string {
return `${userCode}`.padStart(9, '0');
}
export function makeUuid(): string {
const bytes = Buffer.alloc(16);
let timestamp = Date.now();
for (let i = 0; i < bytes.length; ++i) {
const timeByte = timestamp & 0xff;
bytes[i] = (Math.floor(Math.random() * 256) ^ timeByte) & 0xff;
timestamp = Math.floor(timestamp / 256);
}
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = bytes.toString('hex');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
12,
16
)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
export function defaultAvatar(): AvatarSetting {
return {
physique: 0,
skinColor: 0,
face: 0,
eyeColor: 0,
hairstyle: 0,
hairColor: 0,
costumeUpper: 0,
costumeLower: 0,
accessoryHead: 0,
accessoryFace: 0,
accessoryBody: 0,
accessoryHand: 0,
baseChara: 1,
};
}
export function defaultPlayOption(): PlayOptionSetting {
return {
noteSpeed: 0,
noteSize: 0,
judgeSeType: 0,
judgeSeVolume: 5,
judgeTiming: 0,
noteTiming: 0,
guide: 0,
invert: 0,
stealth: 0,
timelineDisplay: 0,
avatarAction: 0,
timelineModelDirection: 0,
avatarModelDirection: 0,
};
}
export function defaultPlayInfo(model = ''): PlayInfo {
return {
locationId: '',
modeId: -1,
styleId: 0,
folderId: 0,
musicId: 0,
// The managed response parser throws on values outside the three XRPC
// fumen names, including an empty string on a brand-new profile.
fumenType: 'BASIC',
startDate: 0,
endDate: 0,
lightPlayCount: 0,
standardPlayCount: 0,
trainingPlayCount: 0,
pcbId: '',
softcode: model,
};
}
export function defaultDancerGrade(): DancerGrade {
return {
grade: 0,
gvGauge: 0,
achievedGv: 0,
receivedGv: 0,
increaseGv: 0,
};
}
export function createProfile(
refid: string,
name: string,
dataId = '',
cardNumber = '',
model = ''
): ProfileDocument {
const createdAt = nowMs();
const userCode = makeUserCode(refid);
return {
collection: 'profile',
schemaVersion: PROFILE_SCHEMA_VERSION,
name: name || 'PLAYER',
dataId,
cardNumber,
userCode,
dancerId: makeDancerId(userCode),
privacy: { publishSetting: 0 },
dancerGrade: defaultDancerGrade(),
avatar: defaultAvatar(),
playOption: defaultPlayOption(),
playInfo: defaultPlayInfo(model),
unlockedMusics: [],
viewFlags: [],
musicUnlockGauges: [],
deltaGroup: [],
loginCount: 0,
createdAt,
updatedAt: createdAt,
};
}
export function normalizeFumen(value: string): FumenType {
if (value === 'ADVANCED' || value === 'MASTER') {
return value;
}
return 'BASIC';
}
export function readNumber(
reader: KDataReader,
path: string,
fallback = 0
): number {
return reader.number(path, fallback);
}
export function readString(
reader: KDataReader,
path: string,
fallback = ''
): string {
return reader.str(path, fallback);
}
export function readAvatar(reader: KDataReader, prefix: string): AvatarSetting {
const p = prefix ? `${prefix}.` : '';
return {
physique: readNumber(reader, `${p}physique`),
skinColor: readNumber(reader, `${p}skin_color`),
face: readNumber(reader, `${p}face`),
eyeColor: readNumber(reader, `${p}eye_color`),
hairstyle: readNumber(reader, `${p}hairstyle`),
hairColor: readNumber(reader, `${p}hair_color`),
costumeUpper: readNumber(reader, `${p}costume_upper`),
costumeLower: readNumber(reader, `${p}costume_lower`),
accessoryHead: readNumber(reader, `${p}accessory_head`),
accessoryFace: readNumber(reader, `${p}accessory_face`),
accessoryBody: readNumber(reader, `${p}accessory_body`),
accessoryHand: readNumber(reader, `${p}accessory_hand`),
baseChara: readNumber(reader, `${p}base_chara`, 1),
};
}
export function readPlayOption(
reader: KDataReader,
prefix: string
): PlayOptionSetting {
const p = prefix ? `${prefix}.` : '';
return {
noteSpeed: readNumber(reader, `${p}note_speed`),
noteSize: readNumber(reader, `${p}note_size`),
judgeSeType: readNumber(reader, `${p}judge_se_type`),
judgeSeVolume: readNumber(reader, `${p}judge_se_vol`, 5),
judgeTiming: readNumber(reader, `${p}judge_timing`),
noteTiming: readNumber(reader, `${p}note_timing`),
guide: readNumber(reader, `${p}guide`),
invert: readNumber(reader, `${p}invert`),
stealth: readNumber(reader, `${p}stealth`),
timelineDisplay: readNumber(reader, `${p}timeline_disp`),
avatarAction: readNumber(reader, `${p}avatar_action`),
timelineModelDirection: readNumber(reader, `${p}timeline_model_dir`),
avatarModelDirection: readNumber(reader, `${p}avatar_model_dir`),
};
}
export function readPlayInfo(
reader: KDataReader,
prefix: string,
model = ''
): PlayInfo {
const p = prefix ? `${prefix}.` : '';
return {
locationId: readString(reader, `${p}loc_id`),
modeId: readNumber(reader, `${p}mode_id`, -1),
styleId: readNumber(reader, `${p}style_id`),
folderId: readNumber(reader, `${p}folder_id`),
musicId: readNumber(reader, `${p}music_id`),
fumenType: normalizeFumen(readString(reader, `${p}fumen_type`, 'BASIC')),
startDate: readNumber(reader, `${p}start_date`),
endDate: readNumber(reader, `${p}end_date`),
lightPlayCount: readNumber(reader, `${p}light_play_count`),
standardPlayCount: readNumber(reader, `${p}standard_play_count`),
trainingPlayCount: readNumber(reader, `${p}training_play_count`),
pcbId: readString(reader, `${p}pcb_id`),
softcode: readString(reader, `${p}softcode`, model),
};
}
export function readStageResult(reader: KDataReader, prefix = ''): StageResult {
const p = prefix ? `${prefix}.` : '';
return {
musicId: readNumber(reader, `${p}music_id`),
fumenType: normalizeFumen(readString(reader, `${p}fumen_type`, 'BASIC')),
clearStatus: readNumber(reader, `${p}clear_status`),
score: readNumber(reader, `${p}score`),
rank: readNumber(reader, `${p}rank`),
combo: readNumber(reader, `${p}combo`),
perfect: readNumber(reader, `${p}perfect`),
great: readNumber(reader, `${p}great`),
good: readNumber(reader, `${p}good`),
bad: readNumber(reader, `${p}bad`),
};
}
export function readMusicScore(reader: KDataReader): MusicScoreEntry {
const playResult = readStageResult(reader);
const playDate = readNumber(reader, 'play_date', nowMs());
return {
...playResult,
gameStartDate: readNumber(reader, 'start_date'),
mode: readNumber(reader, 'mode'),
style: readNumber(reader, 'style'),
stageNo: readNumber(reader, 'stage_no'),
playCount: readNumber(reader, 'play_cnt', 1),
playDate,
bestScoreDate: playDate,
pcbId: '',
locationId: readString(reader, 'loc_id'),
shopName: readString(reader, 'shopname'),
recordCount: readNumber(reader, 'rec_cnt'),
gvScore: 0,
dropFrame: readNumber(reader, 'drop_frame'),
dropFrameMax: readNumber(reader, 'drop_frame_max'),
dropCount: readNumber(reader, 'drop_count'),
videoKey: readString(reader, 'video_key'),
};
}
export function mergeScore(
existing: MusicScoreEntry | undefined,
incoming: MusicScoreEntry
): MusicScoreEntry {
if (!existing) {
return incoming;
}
const isNewBest = incoming.score >= existing.score;
const best = isNewBest ? incoming : existing;
return {
...best,
playCount: Math.max(existing.playCount, incoming.playCount),
playDate: Math.max(existing.playDate, incoming.playDate),
bestScoreDate: isNewBest ? incoming.playDate : existing.bestScoreDate,
clearStatus: Math.max(existing.clearStatus, incoming.clearStatus),
combo: Math.max(existing.combo, incoming.combo),
recordCount: Math.max(existing.recordCount, incoming.recordCount),
gvScore: Math.max(existing.gvScore, incoming.gvScore),
};
}
export function musicScoreKey(
score: Pick
): string {
return `${score.musicId}:${score.fumenType}`;
}
export function readUnlocks(
reader: KDataReader,
path: string
): UnlockMusicEntry[] {
const result: UnlockMusicEntry[] = [];
for (const item of reader.elements(path)) {
const musicId = parseInt(item.attr().music_id || '0', 10);
if (!Number.isFinite(musicId) || musicId <= 0) {
continue;
}
const fumens = item
.elements('fumen_type')
.map(f => normalizeFumen(f.str('', 'BASIC')));
result.push({ musicId, fumens: uniqueFumens(fumens) });
}
return result;
}
export function readViewFlags(
reader: KDataReader,
path: string
): ViewFlagEntry[] {
return reader.elements(path).map(item => ({
viewId: readString(item, 'view_id'),
flag: readNumber(item, 'flag'),
}));
}
export function readMusicUnlockGauges(
reader: KDataReader,
path: string
): MusicUnlockGaugeEntry[] {
return reader.elements(path).map(item => ({
eventId: readString(item, 'event_id'),
progress: readNumber(item, 'progress'),
}));
}
export function readDeltaGroup(
reader: KDataReader,
path: string
): DeltaEntry[] {
return reader.elements(path).map(item => ({
name: readString(item, 'name'),
income: readNumber(item, 'extra_income'),
expense: readNumber(item, 'extra_expense'),
}));
}
export function mergeUnlocks(
left: UnlockMusicEntry[],
right: UnlockMusicEntry[]
): UnlockMusicEntry[] {
const merged: { [musicId: string]: FumenType[] } = {};
for (const entry of [...left, ...right]) {
const key = `${entry.musicId}`;
merged[key] = uniqueFumens([...(merged[key] || []), ...entry.fumens]);
}
return Object.keys(merged)
.map(key => ({ musicId: parseInt(key, 10), fumens: merged[key] }))
.sort((a, b) => a.musicId - b.musicId);
}
function uniqueFumens(values: FumenType[]): FumenType[] {
return ['BASIC', 'ADVANCED', 'MASTER'].filter(
value => values.indexOf(value as FumenType) >= 0
) as FumenType[];
}
export function avatarResponse(avatar: AvatarSetting): any {
return {
physique: K.ITEM('s32', avatar.physique),
skin_color: K.ITEM('s32', avatar.skinColor),
face: K.ITEM('s32', avatar.face),
eye_color: K.ITEM('s32', avatar.eyeColor),
hairstyle: K.ITEM('s32', avatar.hairstyle),
hair_color: K.ITEM('s32', avatar.hairColor),
costume_upper: K.ITEM('s32', avatar.costumeUpper),
costume_lower: K.ITEM('s32', avatar.costumeLower),
accessory_head: K.ITEM('s32', avatar.accessoryHead),
accessory_face: K.ITEM('s32', avatar.accessoryFace),
accessory_body: K.ITEM('s32', avatar.accessoryBody),
accessory_hand: K.ITEM('s32', avatar.accessoryHand),
base_chara: K.ITEM('s32', avatar.baseChara),
};
}
export function playOptionResponse(option: PlayOptionSetting): any {
return {
note_speed: K.ITEM('s32', option.noteSpeed),
note_size: K.ITEM('s32', option.noteSize),
judge_se_type: K.ITEM('s32', option.judgeSeType),
judge_se_vol: K.ITEM('s32', option.judgeSeVolume),
judge_timing: K.ITEM('s32', option.judgeTiming),
note_timing: K.ITEM('s32', option.noteTiming),
guide: K.ITEM('s32', option.guide),
invert: K.ITEM('s32', option.invert),
stealth: K.ITEM('s32', option.stealth),
timeline_disp: K.ITEM('s32', option.timelineDisplay),
avatar_action: K.ITEM('s32', option.avatarAction),
timeline_model_dir: K.ITEM('s32', option.timelineModelDirection),
avatar_model_dir: K.ITEM('s32', option.avatarModelDirection),
};
}
export function dancerGradeResponse(grade: DancerGrade): any {
return {
grade: K.ITEM('s32', grade.grade),
gv_gauge: K.ITEM('s32', grade.gvGauge),
achieve_gv: K.ITEM('s32', grade.achievedGv),
get_gv: K.ITEM('s32', grade.receivedGv),
increase_gv: K.ITEM('s32', grade.increaseGv),
};
}
export function playInfoResponse(info: PlayInfo): any {
return {
loc_id: K.ITEM('str', info.locationId),
mode_id: K.ITEM('s32', info.modeId),
style_id: K.ITEM('s32', info.styleId),
folder_id: K.ITEM('s32', info.folderId),
music_id: K.ITEM('s32', info.musicId),
fumen_type: K.ITEM('str', normalizeFumen(info.fumenType)),
start_date: K.ITEM('s64', BigInt(info.startDate)),
end_date: K.ITEM('s64', BigInt(info.endDate)),
light_play_count: K.ITEM('s32', info.lightPlayCount),
standard_play_count: K.ITEM('s32', info.standardPlayCount),
training_play_count: K.ITEM('s32', info.trainingPlayCount),
pcb_id: K.ITEM('str', info.pcbId),
softcode: K.ITEM('str', info.softcode),
};
}
export function stageResultResponse(result: StageResult): any {
return {
music_id: K.ITEM('s32', result.musicId),
fumen_type: K.ITEM('str', result.fumenType),
clear_status: K.ITEM('s32', result.clearStatus),
score: K.ITEM('s32', result.score),
rank: K.ITEM('s32', result.rank),
combo: K.ITEM('s32', result.combo),
perfect: K.ITEM('s32', result.perfect),
great: K.ITEM('s32', result.great),
good: K.ITEM('s32', result.good),
bad: K.ITEM('s32', result.bad),
};
}
export function motionInfoResponse(entry: MotionEntry): any {
return {
motion_id: K.ITEM('str', entry.motionId),
is_active: K.ITEM('bool', entry.active),
publish_setting: K.ITEM('s32', entry.publishSetting),
play_cnt: K.ITEM('s32', entry.playCount),
total_gv: K.ITEM('s32', entry.totalGv),
...stageResultResponse(entry.playResult),
play_date: K.ITEM('s64', BigInt(entry.playDate)),
loc_id: K.ITEM('str', entry.locationId),
shopname: K.ITEM('str', entry.shopName),
};
}