mirror of
https://github.com/laochan-eacnet/backend.git
synced 2026-09-29 02:08:05 +03:00
Merge pull request #3 from laochan-eacnet/gitadora
Add Gitadora Support
This commit is contained in:
+72780
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ export class DefaultService {
|
||||
|
||||
p2d()(undefined, undefined, descriptor);
|
||||
return descriptor.value();
|
||||
} else if (['sdvx', 'ddr'].includes(ctx.service.name)) {
|
||||
} else if (['sdvx', 'ddr', 'gitadora'].includes(ctx.service.name)) {
|
||||
const result = {
|
||||
status: v.s32(0),
|
||||
error_code: v.s32(0),
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { inject, singleton } from "tsyringe";
|
||||
import { Combine } from "../../utils/combine.js";
|
||||
import { EacGeneric } from "../eac-generic.js";
|
||||
import { User } from "./user.js";
|
||||
import { generic } from "../../decorators/eacnet.js";
|
||||
import { Serializable, v } from "../../utils/kxml-value.js";
|
||||
import { event, musicinfo } from "../../datas/gitadora.js";
|
||||
import { UserService } from "../../services/gitadora/user.js";
|
||||
|
||||
@singleton()
|
||||
export default class extends Combine(EacGeneric, User) {
|
||||
constructor(
|
||||
@inject(UserService) private readonly userService: UserService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.userService;
|
||||
}
|
||||
|
||||
@generic()
|
||||
knst_playablemusic_get(): Serializable {
|
||||
return {
|
||||
status: v.s32(0),
|
||||
error_code: v.s32(0),
|
||||
xrpc_status_code: v.s32(0),
|
||||
xrpc_fault_code: v.s32(0),
|
||||
response: {
|
||||
knst_playablemusic: {
|
||||
$status: 0,
|
||||
musicinfo,
|
||||
hot: {
|
||||
major: v.s32(1),
|
||||
minor: v.s32(0),
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@generic()
|
||||
knst_gameinfo_get(): Serializable {
|
||||
return {
|
||||
status: v.s32(0),
|
||||
error_code: v.s32(0),
|
||||
xrpc_status_code: v.s32(0),
|
||||
xrpc_fault_code: v.s32(0),
|
||||
response: {
|
||||
knst_gameinfo: {
|
||||
$status: 0,
|
||||
now_date: v.u64(Date.now()),
|
||||
extra: {
|
||||
extra_lv: v.u8(0),
|
||||
extramusic: {
|
||||
music: []
|
||||
}
|
||||
},
|
||||
unlock_challenge: {
|
||||
term: v.u8(0),
|
||||
},
|
||||
ctrl_movie: {},
|
||||
ng_jacket: {},
|
||||
// TODO: actual sort the musics by play count
|
||||
ranking: {
|
||||
skill_0_999: {},
|
||||
skill_1000_1499: {},
|
||||
skill_1500_1999: {},
|
||||
skill_2000_2499: {},
|
||||
skill_2500_2999: {},
|
||||
skill_3000_3499: {},
|
||||
skill_3500_3999: {},
|
||||
skill_4000_4499: {},
|
||||
skill_4500_4999: {},
|
||||
skill_5000_5499: {},
|
||||
skill_5500_5999: {},
|
||||
skill_6000_6499: {},
|
||||
skill_6500_6999: {},
|
||||
skill_7000_7499: {},
|
||||
skill_7500_7999: {},
|
||||
skill_8000_8499: {},
|
||||
skill_8500_9999: {},
|
||||
total: {},
|
||||
original: {},
|
||||
bemani: {},
|
||||
famous: {},
|
||||
anime: {},
|
||||
band: {},
|
||||
western: {},
|
||||
},
|
||||
processing_report_state: v.u8(0),
|
||||
data_ver_limit: {
|
||||
type: v.str('data_ver_limit'),
|
||||
term: v.u8(0),
|
||||
state: v.u8(0),
|
||||
start_date: v.str('1970-01-01 09:00:00'),
|
||||
end_date: v.str('1970-01-01 09:00:00'),
|
||||
start_date_ms: v.u64(0),
|
||||
end_date_ms: v.u64(0),
|
||||
},
|
||||
general_term: {},
|
||||
assert_report_state: v.u8(0),
|
||||
rockwave: {
|
||||
event_list: {
|
||||
event,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { generic } from "../../decorators/eacnet.js";
|
||||
import { UserService } from "../../services/gitadora/user.js";
|
||||
import { Context } from "../../types.js";
|
||||
import { Player } from "../../types/gitadora/data.js";
|
||||
import { Serializable, v } from "../../utils/kxml-value.js";
|
||||
import { tokenToHash } from "../../utils/laochan-id.js";
|
||||
|
||||
export class User {
|
||||
userService: UserService;
|
||||
|
||||
@generic()
|
||||
async knst_cardutil_check(ctx: Context): Promise<Serializable> {
|
||||
const player = await this.userService.findPlayer(ctx.token);
|
||||
const type = this.getGameType(ctx);
|
||||
|
||||
return {
|
||||
status: v.s32(0),
|
||||
error_code: v.s32(0),
|
||||
xrpc_status_code: v.s32(0),
|
||||
xrpc_fault_code: v.s32(0),
|
||||
response: {
|
||||
knst_cardutil: {
|
||||
$status: 0,
|
||||
player: {
|
||||
$no: 1,
|
||||
$state: 2,
|
||||
refid: v.str(tokenToHash(ctx.token)),
|
||||
did: v.s32(1),
|
||||
cardnumber: v.str(''),
|
||||
usercode: v.str(tokenToHash(ctx.token)),
|
||||
player_type: v.s8(0),
|
||||
name: v.str(''),
|
||||
title: v.str(''),
|
||||
charaid: v.s32(0),
|
||||
skilldata: {
|
||||
skill: v.s32(player.gameSave[type].skill),
|
||||
all_skill: v.s32(player.gameSave.gf.skill + player.gameSave.dm.skill),
|
||||
old_skill: v.s32(-1),
|
||||
old_all_skill: v.s32(-1),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@generic()
|
||||
knst_cardutil_regist(): Serializable {
|
||||
return {
|
||||
status: v.s32(0),
|
||||
error_code: v.s32(0),
|
||||
xrpc_status_code: v.s32(0),
|
||||
xrpc_fault_code: v.s32(0),
|
||||
response: {
|
||||
knst_cardutil: {
|
||||
$status: 0,
|
||||
is_succession: v.bool(false),
|
||||
did: v.s32(1),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getGameType(ctx: Context): 'gf' | 'dm' {
|
||||
if (typeof ctx.body.gametype !== 'string') {
|
||||
throw new Error('invaild request');
|
||||
}
|
||||
|
||||
if (ctx.body.gametype == 'U32:J:A') {
|
||||
return 'gf';
|
||||
} else if (ctx.body.gametype == 'U32:J:B') {
|
||||
return 'dm';
|
||||
}
|
||||
|
||||
throw new Error('unknown game type');
|
||||
}
|
||||
|
||||
@generic()
|
||||
async knst_gametop_get(ctx: Context): Promise<Serializable> {
|
||||
const type = this.getGameType(ctx);
|
||||
const [data, musics] = await Promise.all([
|
||||
this.userService.findPlayer(ctx.token),
|
||||
this.userService.findMusicDatas(ctx.token, type),
|
||||
]);
|
||||
|
||||
const save = data.gameSave[type];
|
||||
|
||||
return {
|
||||
status: v.s32(0),
|
||||
error_code: v.s32(0),
|
||||
xrpc_status_code: v.s32(0),
|
||||
xrpc_fault_code: v.s32(0),
|
||||
response: {
|
||||
knst_gametop: {
|
||||
$status: 0,
|
||||
player: {
|
||||
now_date: v.u64(Date.now()),
|
||||
rq_key: v.str('dummy-req-key'),
|
||||
player_info: {
|
||||
refid: v.str(tokenToHash(ctx.token)),
|
||||
did: v.s32(1),
|
||||
cardnumber: v.str(''),
|
||||
usercode: v.str(tokenToHash(ctx.token).slice(0, 10)),
|
||||
player_type: v.s8(0),
|
||||
name: v.str(''),
|
||||
title: v.str(''),
|
||||
charaid: v.s32(0),
|
||||
},
|
||||
playinfo: Object.fromEntries(
|
||||
Object.entries(save.playInfo).map(kv => [kv[0], v.s32(kv[1])])
|
||||
),
|
||||
reward: {
|
||||
status: v.u32(save.rewardStatus),
|
||||
},
|
||||
customdata: {
|
||||
playstyle: v.s32(save.customData.playStyle),
|
||||
custom: v.s32(save.customData.custom),
|
||||
},
|
||||
skilldata: {
|
||||
skill: v.s32(save.skill),
|
||||
all_skill: v.s32(data.gameSave.gf.skill + data.gameSave.dm.skill),
|
||||
old_skill: v.s32(-1),
|
||||
old_all_skill: v.s32(-1),
|
||||
},
|
||||
ranking: {
|
||||
skill: {
|
||||
rank: v.s32(1),
|
||||
total_nr: v.s32(1),
|
||||
},
|
||||
all_skill: {
|
||||
rank: v.s32(1),
|
||||
total_nr: v.s32(1),
|
||||
}
|
||||
},
|
||||
secretmusic: {
|
||||
music: data.secretMusics.map(m => ({
|
||||
musicid: v.s32(m),
|
||||
seq: v.u16(255),
|
||||
kind: v.s32(1),
|
||||
})),
|
||||
},
|
||||
favoritemusic: {
|
||||
list_1: v.s32(save.favoriteMusic.list1),
|
||||
list_2: v.s32(save.favoriteMusic.list2),
|
||||
list_3: v.s32(save.favoriteMusic.list3),
|
||||
},
|
||||
information: {
|
||||
info: v.u32(save.information),
|
||||
},
|
||||
record: {
|
||||
gf: {
|
||||
max_record: Object.fromEntries(
|
||||
Object.entries(data.gameSave.gf.maxRecord).map(kv => [kv[0], v.s32(kv[1])])
|
||||
),
|
||||
diff_record: Object.fromEntries(
|
||||
Object.entries(data.gameSave.gf.diffRecord).map(kv => [kv[0], v.s32(kv[1])])
|
||||
),
|
||||
},
|
||||
dm: {
|
||||
max_record: Object.fromEntries(
|
||||
Object.entries(data.gameSave.dm.maxRecord).map(kv => [kv[0], v.s32(kv[1])])
|
||||
),
|
||||
diff_record: Object.fromEntries(
|
||||
Object.entries(data.gameSave.dm.diffRecord).map(kv => [kv[0], v.s32(kv[1])])
|
||||
),
|
||||
},
|
||||
},
|
||||
stage_result: {},
|
||||
rockwave: {},
|
||||
mp_list: {},
|
||||
musiclist: {
|
||||
$nr: musics.length,
|
||||
musicdata: musics.map(m => {
|
||||
let bestSeq = -1;
|
||||
let bestScore = -1;
|
||||
const mdata = [
|
||||
-1,
|
||||
-2, -2, -2, -2, -2, -2, -2, -2,
|
||||
-1, -1, -1, -1, -1, -1, -1, -1,
|
||||
];
|
||||
const meter = new Array(8).fill(0);
|
||||
const meterProg = new Array(8).fill(0);
|
||||
const flags = new Array(4).fill(0);
|
||||
|
||||
for (const seq in m.seqs) {
|
||||
const rec = m.seqs[seq];
|
||||
if (rec.skill > bestScore) {
|
||||
bestScore = rec.skill;
|
||||
bestSeq = +seq;
|
||||
}
|
||||
|
||||
mdata[+seq] = rec.clear ? rec.perc : -1;
|
||||
mdata[8 + (+seq)] = rec.rank;
|
||||
meter[+seq] = rec.meter;
|
||||
meterProg[+seq] = rec.meterProgress;
|
||||
|
||||
if (rec.fullcombo) {
|
||||
flags[0] |= 1 << +seq;
|
||||
}
|
||||
|
||||
if (rec.excellent) {
|
||||
flags[1] |= 1 << +seq;
|
||||
}
|
||||
|
||||
if (rec.clear) {
|
||||
flags[2] |= 1 << +seq;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
$musicid: m.musicId,
|
||||
flag: v.u16(flags),
|
||||
sdata: v.s16([bestSeq, bestScore]),
|
||||
mdata: v.s16(mdata),
|
||||
meter: v.u64(meter),
|
||||
meter_prog: v.s16(meterProg),
|
||||
}
|
||||
}),
|
||||
},
|
||||
finish: v.bool(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@generic()
|
||||
async knst_gameend_regist(ctx: Context): Promise<Serializable> {
|
||||
const type = this.getGameType(ctx);
|
||||
const player = ctx.body.player as Player;
|
||||
|
||||
const stages = ctx.body.player.stage ? (
|
||||
ctx.body.player.stage instanceof Array ?
|
||||
ctx.body.player.stage : [ctx.body.player.stage]
|
||||
) : [];
|
||||
|
||||
await Promise.all([
|
||||
stages.map(m => this.userService.updateMusicData(ctx.token, m, type)),
|
||||
this.userService.upsertPlayer(ctx.token, {
|
||||
secretMusics: player.secretmusic.music.map(v => v.musicid),
|
||||
gameSave: {
|
||||
[type]: {
|
||||
playInfo: player.playinfo,
|
||||
rewardStatus: player.reward.status,
|
||||
customData: {
|
||||
playStyle: player.customdata.playstyle,
|
||||
custom: player.customdata.custom,
|
||||
},
|
||||
skill: player.skilldata.skill,
|
||||
favoriteMusic: {
|
||||
list1: player.favoritemusic.music_list_1,
|
||||
list2: player.favoritemusic.music_list_2,
|
||||
list3: player.favoritemusic.music_list_3,
|
||||
},
|
||||
information: player.information.info,
|
||||
maxRecord: player.record.max,
|
||||
diffRecord: player.record.diff,
|
||||
},
|
||||
},
|
||||
})
|
||||
]);
|
||||
|
||||
return {
|
||||
status: v.s32(0),
|
||||
error_code: v.s32(0),
|
||||
xrpc_status_code: v.s32(0),
|
||||
xrpc_fault_code: v.s32(0),
|
||||
response: {
|
||||
knst_gameend: {
|
||||
$status: 0,
|
||||
gamemode: {
|
||||
$mode: 'game_mode',
|
||||
},
|
||||
player: {
|
||||
$no: 1,
|
||||
skill: {
|
||||
rank: v.s32(1),
|
||||
total_nr: v.s32(1),
|
||||
},
|
||||
all_skill: {
|
||||
rank: v.s32(1),
|
||||
total_nr: v.s32(1),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { parser, toObject } from "../utils/kbinxml.js";
|
||||
import { Serializable, v } from "../utils/kxml-value.js";
|
||||
import { Mdb } from "../types/gitadora/data.js";
|
||||
|
||||
export const musicinfo: Serializable = (() => {
|
||||
if (!existsSync('data/mdb_knst.xml')) {
|
||||
throw new Error('data/mdb_knst.xml not found!!');
|
||||
}
|
||||
|
||||
const xml = readFileSync('data/mdb_knst.xml');
|
||||
const mdb = toObject(parser.parse(xml)).mdb as Mdb;
|
||||
|
||||
return {
|
||||
$nr: mdb.mdb_data.length,
|
||||
music: mdb.mdb_data.map(m => ({
|
||||
id: v.s32(m.music_id),
|
||||
diff: v.u16(m.xg_diff_list),
|
||||
cont_gf: v.bool(!!m.contain_stat[0]),
|
||||
cont_dm: v.bool(!!m.contain_stat[1]),
|
||||
is_cont: v.bool(true),
|
||||
is_secret: v.bool(false),
|
||||
is_hot: v.bool(((m.contain_stat[0] & 1) != 0) || ((m.contain_stat[1] & 1) != 0)),
|
||||
data_ver: v.s32(m.data_ver),
|
||||
knst_pack: v.s32(0), // v.s32(m.knst_pack),
|
||||
ifs_ver: v.s32(m.ifs_ver),
|
||||
}))
|
||||
}
|
||||
})();
|
||||
|
||||
export const event = [{
|
||||
data_id: v.s32(8),
|
||||
data_version: v.s32(200),
|
||||
event_id: v.s32(0),
|
||||
event_type: v.s32(18),
|
||||
start_date: v.u64(1615510800000),
|
||||
end_date: v.u64(4771493999000),
|
||||
is_open: v.bool(true),
|
||||
bg_no: v.s32(0),
|
||||
target_musicid: v.s32([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]),
|
||||
clear_border: v.s32(2),
|
||||
reward_musicid: v.s32([-1, -1, 2708, 2709]),
|
||||
reward_stickerid: v.s32(-1),
|
||||
firstbit: v.s32(0),
|
||||
event_skill_id: v.s32(0),
|
||||
quest_no: v.s32(0),
|
||||
url: v.str(''),
|
||||
},
|
||||
{
|
||||
data_id: v.s32(9),
|
||||
data_version: v.s32(200),
|
||||
event_id: v.s32(0),
|
||||
event_type: v.s32(18),
|
||||
start_date: v.u64(1618966800000),
|
||||
end_date: v.u64(4771493999000),
|
||||
is_open: v.bool(true),
|
||||
bg_no: v.s32(0),
|
||||
target_musicid: v.s32([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]),
|
||||
clear_border: v.s32(2),
|
||||
reward_musicid: v.s32([-1, -1, -1, 2710]),
|
||||
reward_stickerid: v.s32(-1),
|
||||
firstbit: v.s32(0),
|
||||
event_skill_id: v.s32(0),
|
||||
quest_no: v.s32(0),
|
||||
url: v.str(''),
|
||||
},
|
||||
{
|
||||
data_id: v.s32(87),
|
||||
data_version: v.s32(220),
|
||||
event_id: v.s32(0),
|
||||
event_type: v.s32(3),
|
||||
start_date: v.u64(1743469200000),
|
||||
end_date: v.u64(1745765999000),
|
||||
is_open: v.bool(true),
|
||||
bg_no: v.s32(0),
|
||||
target_musicid: v.s32([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]),
|
||||
clear_border: v.s32(25000000),
|
||||
reward_musicid: v.s32([-1, -1, -1, 1118]),
|
||||
reward_stickerid: v.s32(-1),
|
||||
firstbit: v.s32(0),
|
||||
event_skill_id: v.s32(0),
|
||||
quest_no: v.s32(62),
|
||||
url: v.str(''),
|
||||
}];
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LZ77 } from '../utils/lz77.js';
|
||||
import { toKBinXml } from '../utils/kbinxml.js';
|
||||
import config from '../utils/config.js';
|
||||
|
||||
export function eacnet(protocol_name: string, topName: string, dataOffset: number) {
|
||||
return (encoding: 'UTF-8' | 'SHIFT_JIS' = 'UTF-8') => {
|
||||
@@ -8,7 +9,7 @@ export function eacnet(protocol_name: string, topName: string, dataOffset: numbe
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const obj = await orig.apply(this, args);
|
||||
|
||||
const kxml = toKBinXml(topName, obj, encoding);
|
||||
const kxml = toKBinXml(topName, obj, encoding, config.isDev);
|
||||
const compressed = LZ77.compress(kxml.data);
|
||||
const result = Buffer.allocUnsafe(compressed.length + dataOffset);
|
||||
|
||||
|
||||
@@ -118,6 +118,8 @@ async function main(): Promise<void> {
|
||||
return '<ITEM>';
|
||||
} else if (key == 'param') {
|
||||
return '<PARAM>';
|
||||
} else if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
return value;
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Db, UpdateResult } from "mongodb";
|
||||
import { inject, injectable } from "tsyringe";
|
||||
import { GameSave, MusicData, PlayerData, PlayRecord, Stage } from "../../types/gitadora/index.js";
|
||||
|
||||
@injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@inject(Db)
|
||||
private readonly db: Db,
|
||||
) { }
|
||||
|
||||
get playerDataCol() {
|
||||
return this.db.collection<PlayerData>('gitadora_play_data');
|
||||
}
|
||||
|
||||
musicDataCol(type: 'gf' | 'dm') {
|
||||
return this.db.collection<MusicData>('gitadora_' + type + '_music_data');
|
||||
}
|
||||
|
||||
playRecordCol(type: 'gf' | 'dm') {
|
||||
return this.db.collection<PlayRecord>('gitadora_' + type + '_play_record');
|
||||
}
|
||||
|
||||
private emptyGameSave(): GameSave {
|
||||
return {
|
||||
playInfo: {
|
||||
cabid: 0,
|
||||
play: 0,
|
||||
playtime: 0,
|
||||
playterm: 0,
|
||||
session_cnt: 0,
|
||||
matching_num: 0,
|
||||
extra_stage: 0,
|
||||
extra_play: 0,
|
||||
extra_clear: 0,
|
||||
encore_play: 0,
|
||||
encore_clear: 0,
|
||||
pencore_play: 0,
|
||||
pencore_clear: 0,
|
||||
max_clear_diff: 0,
|
||||
max_full_diff: 0,
|
||||
max_exce_diff: 0,
|
||||
clear_num: 0,
|
||||
full_num: 0,
|
||||
exce_num: 0,
|
||||
no_num: 0,
|
||||
e_num: 0,
|
||||
d_num: 0,
|
||||
c_num: 0,
|
||||
b_num: 0,
|
||||
a_num: 0,
|
||||
s_num: 0,
|
||||
ss_num: 0,
|
||||
last_category: 0,
|
||||
last_musicid: -1,
|
||||
last_seq: 0,
|
||||
disp_level: 0,
|
||||
},
|
||||
rewardStatus: new Array(50).fill(0),
|
||||
customData: {
|
||||
playStyle: new Array(50).fill(0),
|
||||
custom: new Array(50).fill(0),
|
||||
},
|
||||
skill: 0,
|
||||
favoriteMusic: {
|
||||
list1: new Array(100).fill(-1),
|
||||
list2: new Array(100).fill(-1),
|
||||
list3: new Array(100).fill(-1),
|
||||
},
|
||||
information: new Array(50).fill(0),
|
||||
maxRecord: {
|
||||
skill: 0,
|
||||
all_skill: 0,
|
||||
clear_diff: 0,
|
||||
full_diff: 0,
|
||||
exce_diff: 0,
|
||||
clear_music_num: 0,
|
||||
full_music_num: 0,
|
||||
exce_music_num: 0,
|
||||
clear_seq_num: 0,
|
||||
classic_all_skill: 0,
|
||||
},
|
||||
diffRecord: {
|
||||
diff_100_nr: 0,
|
||||
diff_150_nr: 0,
|
||||
diff_200_nr: 0,
|
||||
diff_250_nr: 0,
|
||||
diff_300_nr: 0,
|
||||
diff_350_nr: 0,
|
||||
diff_400_nr: 0,
|
||||
diff_450_nr: 0,
|
||||
diff_500_nr: 0,
|
||||
diff_550_nr: 0,
|
||||
diff_600_nr: 0,
|
||||
diff_650_nr: 0,
|
||||
diff_700_nr: 0,
|
||||
diff_750_nr: 0,
|
||||
diff_800_nr: 0,
|
||||
diff_850_nr: 0,
|
||||
diff_900_nr: 0,
|
||||
diff_950_nr: 0,
|
||||
diff_100_clear: new Array(7).fill(0),
|
||||
diff_150_clear: new Array(7).fill(0),
|
||||
diff_200_clear: new Array(7).fill(0),
|
||||
diff_250_clear: new Array(7).fill(0),
|
||||
diff_300_clear: new Array(7).fill(0),
|
||||
diff_350_clear: new Array(7).fill(0),
|
||||
diff_400_clear: new Array(7).fill(0),
|
||||
diff_450_clear: new Array(7).fill(0),
|
||||
diff_500_clear: new Array(7).fill(0),
|
||||
diff_550_clear: new Array(7).fill(0),
|
||||
diff_600_clear: new Array(7).fill(0),
|
||||
diff_650_clear: new Array(7).fill(0),
|
||||
diff_700_clear: new Array(7).fill(0),
|
||||
diff_750_clear: new Array(7).fill(0),
|
||||
diff_800_clear: new Array(7).fill(0),
|
||||
diff_850_clear: new Array(7).fill(0),
|
||||
diff_900_clear: new Array(7).fill(0),
|
||||
diff_950_clear: new Array(7).fill(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findPlayer(token: string): Promise<PlayerData> {
|
||||
const player = await this.playerDataCol.findOne({ _id: token }) ?? {
|
||||
_id: token,
|
||||
gameSave: {},
|
||||
secretMusics: [],
|
||||
} as PlayerData;
|
||||
|
||||
if (!player.gameSave.gf) {
|
||||
player.gameSave.gf = this.emptyGameSave();
|
||||
}
|
||||
|
||||
if (!player.gameSave.dm) {
|
||||
player.gameSave.dm = this.emptyGameSave();
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
upsertPlayer(token: string, data: Partial<PlayerData>): Promise<UpdateResult<PlayerData>> {
|
||||
return this.playerDataCol.updateOne({ _id: token }, {
|
||||
$set: data,
|
||||
}, { upsert: true });
|
||||
}
|
||||
|
||||
findMusicDatas(token: string, type: 'gf' | 'dm'): Promise<MusicData[]> {
|
||||
return this.musicDataCol(type).find({ player: token }, { useBigInt64: true }).toArray();
|
||||
}
|
||||
|
||||
async updateMusicData(token: string, stage: Stage, type: 'gf' | 'dm'): Promise<void> {
|
||||
// make sure meter is bigint
|
||||
stage.meter = BigInt(stage.meter);
|
||||
|
||||
const [data, result] = await Promise.all([
|
||||
(async () => await this.musicDataCol(type).findOne({
|
||||
player: token,
|
||||
musicId: stage.musicid,
|
||||
}, { useBigInt64: true }) ?? {
|
||||
player: token,
|
||||
musicId: stage.musicid,
|
||||
seqs: {},
|
||||
} as MusicData)(),
|
||||
this.playRecordCol(type).insertOne({
|
||||
player: token,
|
||||
...stage
|
||||
}, { useBigInt64: true }),
|
||||
]);
|
||||
|
||||
if (stage.seq in data.seqs) {
|
||||
const seqData = data.seqs[stage.seq];
|
||||
|
||||
seqData.fullcombo = seqData.fullcombo || stage.fullcombo;
|
||||
seqData.excellent = seqData.excellent || stage.excellent;
|
||||
seqData.clear = seqData.clear || stage.clear;
|
||||
|
||||
if (stage.perc > seqData.perc) {
|
||||
seqData.perc = stage.perc;
|
||||
seqData.bestRecord = result.insertedId;
|
||||
}
|
||||
|
||||
seqData.rank = Math.max(seqData.rank, stage.rank);
|
||||
seqData.skill = Math.max(seqData.skill, stage.score);
|
||||
|
||||
if (stage.meter_prog > seqData.meterProgress) {
|
||||
seqData.meterProgress = stage.meter_prog;
|
||||
seqData.meter = stage.meter;
|
||||
}
|
||||
} else {
|
||||
data.seqs[stage.seq] = {
|
||||
fullcombo: stage.fullcombo,
|
||||
excellent: stage.excellent,
|
||||
clear: stage.clear,
|
||||
skill: stage.skill,
|
||||
perc: stage.perc,
|
||||
rank: stage.rank,
|
||||
meter: stage.meter,
|
||||
meterProgress: stage.meter_prog,
|
||||
bestRecord: result.insertedId,
|
||||
}
|
||||
}
|
||||
|
||||
await this.musicDataCol(type).updateOne({ player: token, musicId: stage.musicid }, {
|
||||
$set: data,
|
||||
}, { useBigInt64: true, upsert: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
|
||||
export interface Mdb {
|
||||
header: {
|
||||
data: {
|
||||
id: number[];
|
||||
format: number;
|
||||
chksum: number;
|
||||
header_sz: number;
|
||||
record_sz: number;
|
||||
record_nr: number;
|
||||
course_sz: number;
|
||||
course_nr: number;
|
||||
}
|
||||
}
|
||||
mdb_data: MdbData[];
|
||||
mdb_course: MdbCourse[];
|
||||
}
|
||||
|
||||
export interface MdbCourse {
|
||||
course_id: number;
|
||||
course_flag: number;
|
||||
music_id: number[];
|
||||
classics_diff_list: number[];
|
||||
}
|
||||
|
||||
export interface MdbData {
|
||||
music_id: number;
|
||||
classics_diff_list: number[];
|
||||
xg_diff_list: number[];
|
||||
pad_diff: number;
|
||||
seq_flag: number;
|
||||
xg_seq_flag: number;
|
||||
contain_stat: number[];
|
||||
first_ver: number[];
|
||||
first_classic_ver: number[];
|
||||
b_long: boolean;
|
||||
b_eemall: boolean;
|
||||
b_konasta: boolean;
|
||||
bpm: number;
|
||||
bpm2: number;
|
||||
title_ascii?: string;
|
||||
order_ascii: number;
|
||||
order_kana: number;
|
||||
category_kana: number;
|
||||
artist_title_ascii?: string;
|
||||
artist_order_ascii: number;
|
||||
artist_order_kana: number;
|
||||
artist_category_kana: number;
|
||||
secret: number[];
|
||||
xg_secret: number[];
|
||||
b_session: boolean;
|
||||
xg_b_session: boolean;
|
||||
speed: number;
|
||||
life: number;
|
||||
gf_ofst: number;
|
||||
dm_ofst: number;
|
||||
chart_list: number[];
|
||||
origin: number;
|
||||
music_type: number;
|
||||
genre: number;
|
||||
xg_active_effect_type: number;
|
||||
xg_movie_disp_type: number;
|
||||
xg_movie_disp_id: number;
|
||||
is_remaster: number;
|
||||
title_name: string;
|
||||
license_disp: number;
|
||||
default_music: number[];
|
||||
disable_area: number[];
|
||||
type_category: number;
|
||||
data_ver: number;
|
||||
seq_id: number;
|
||||
is_classic_seq: number;
|
||||
knst_pack: number;
|
||||
ifs_ver: number;
|
||||
knst_course: number;
|
||||
}
|
||||
|
||||
export interface Stage {
|
||||
date_ms: number;
|
||||
stage_no: number;
|
||||
musicid: number;
|
||||
seq: number;
|
||||
skill: number;
|
||||
new_skill: number;
|
||||
clear: boolean;
|
||||
auto_clear: boolean;
|
||||
fullcombo: boolean;
|
||||
excellent: boolean;
|
||||
medal: number;
|
||||
perc: number;
|
||||
new_perc: number;
|
||||
rank: number;
|
||||
score: number;
|
||||
combo: number;
|
||||
max_combo_perc: number;
|
||||
flags: number;
|
||||
phrase_combo_perc: number;
|
||||
perfect: number;
|
||||
great: number;
|
||||
good: number;
|
||||
ok: number;
|
||||
miss: number;
|
||||
perfect_perc: number;
|
||||
great_perc: number;
|
||||
good_perc: number;
|
||||
ok_perc: number;
|
||||
miss_perc: number;
|
||||
meter: number | bigint;
|
||||
meter_prog: number;
|
||||
before_meter: number;
|
||||
before_meter_prog: number;
|
||||
is_new_meter: boolean;
|
||||
phrase_data_num: number;
|
||||
phrase_addr: number[];
|
||||
phrase_type: number[];
|
||||
phrase_status: number[];
|
||||
phrase_end_addr: number;
|
||||
session: Record<'total' | 'gf1' | 'gf2' | 'dm', {
|
||||
perc: number;
|
||||
perfect: number;
|
||||
combo: number;
|
||||
synchro_meter: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
play_mode: number;
|
||||
rq_key: string;
|
||||
refid: string;
|
||||
charaid: number;
|
||||
used_app_point: number;
|
||||
card_no: string;
|
||||
customdata: Customdata;
|
||||
playinfo: { [key: string]: number };
|
||||
title_parts: TitleParts;
|
||||
secretmusic: Secretmusic;
|
||||
tutorial: Tutorial;
|
||||
information: Information;
|
||||
reward: Reward;
|
||||
skilldata: Skilldata;
|
||||
groove: Groove;
|
||||
record: {
|
||||
max: Record<string, number>;
|
||||
diff: Record<string, number | number[]>;
|
||||
};
|
||||
battledata: Battledata;
|
||||
sessiondata: Sessiondata;
|
||||
favoritemusic: Favoritemusic;
|
||||
recommend_musicid_list: number[];
|
||||
thanks_medal: ThanksMedal;
|
||||
skindata: Skindata;
|
||||
monthly_skill: MonthlySkill;
|
||||
event_skill: EventSkill;
|
||||
phrase_combo_challenge: PhraseComboChallenge;
|
||||
phrase_combo_challenge_2: PhraseComboChallenge;
|
||||
phrase_combo_challenge_3: PhraseComboChallenge;
|
||||
phrase_combo_challenge_4: PhraseComboChallenge;
|
||||
phrase_combo_challenge_5: PhraseComboChallenge;
|
||||
phrase_combo_challenge_6: PhraseComboChallenge;
|
||||
phrase_combo_challenge_7: PhraseComboChallenge;
|
||||
phrase_combo_challenge_8: PhraseComboChallenge;
|
||||
phrase_combo_challenge_9: PhraseComboChallenge;
|
||||
phrase_combo_challenge_10: PhraseComboChallenge;
|
||||
phrase_combo_challenge_11: PhraseComboChallenge;
|
||||
phrase_combo_challenge_12: PhraseComboChallenge;
|
||||
phrase_combo_challenge_13: PhraseComboChallenge;
|
||||
phrase_combo_challenge_14: PhraseComboChallenge;
|
||||
phrase_combo_challenge_15: PhraseComboChallenge;
|
||||
phrase_combo_challenge_16: PhraseComboChallenge;
|
||||
phrase_combo_challenge_17: PhraseComboChallenge;
|
||||
phrase_combo_challenge_18: PhraseComboChallenge;
|
||||
phrase_combo_challenge_19: PhraseComboChallenge;
|
||||
phrase_combo_challenge_20: PhraseComboChallenge;
|
||||
rockwave: Rockwave;
|
||||
stage: Stage;
|
||||
card: string;
|
||||
no: string;
|
||||
}
|
||||
|
||||
export interface Battledata {
|
||||
info: Info;
|
||||
setting: Setting;
|
||||
score: BattledataScore;
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
orb: number;
|
||||
get_gb_point: number;
|
||||
send_gb_point: number;
|
||||
}
|
||||
|
||||
export interface BattledataScore {
|
||||
battle_class: number;
|
||||
max_battle_class: number;
|
||||
battle_point: number;
|
||||
win: number;
|
||||
lose: number;
|
||||
draw: number;
|
||||
consecutive_win: number;
|
||||
max_consecutive_win: number;
|
||||
glorious_win: number;
|
||||
max_defeat_skill: number;
|
||||
latest_result: number;
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
matching: number;
|
||||
info_level: number;
|
||||
}
|
||||
|
||||
export interface Customdata {
|
||||
playstyle: number[];
|
||||
custom: number[];
|
||||
}
|
||||
|
||||
export interface EventSkill {
|
||||
skill: number;
|
||||
target_skill_eventid: number[];
|
||||
}
|
||||
|
||||
export interface Favoritemusic {
|
||||
music_list_1: number[];
|
||||
music_list_2: number[];
|
||||
music_list_3: number[];
|
||||
}
|
||||
|
||||
export interface Groove {
|
||||
extra_gauge: number;
|
||||
encore_gauge: number;
|
||||
encore_cnt: number;
|
||||
encore_success: number;
|
||||
unlock_point: number;
|
||||
}
|
||||
|
||||
export interface Information {
|
||||
info: number[];
|
||||
}
|
||||
|
||||
export interface MonthlySkill {
|
||||
term: number;
|
||||
total_skill: number;
|
||||
target_score: TargetScore;
|
||||
}
|
||||
|
||||
export interface TargetScore {
|
||||
music: TargetScoreMusic[];
|
||||
}
|
||||
|
||||
export interface TargetScoreMusic {
|
||||
seq: number;
|
||||
perc: number;
|
||||
skill: number;
|
||||
}
|
||||
|
||||
export interface PhraseComboChallenge {
|
||||
point: number;
|
||||
}
|
||||
|
||||
export interface Reward {
|
||||
status: number[];
|
||||
}
|
||||
|
||||
export interface Rockwave {
|
||||
score_list: ScoreList;
|
||||
}
|
||||
|
||||
export interface ScoreList {
|
||||
score: ScoreElement[];
|
||||
}
|
||||
|
||||
export interface ScoreElement {
|
||||
data_id: number;
|
||||
point: number[];
|
||||
mtime: number;
|
||||
play_cnt: number;
|
||||
is_clear: boolean;
|
||||
}
|
||||
|
||||
export interface Secretmusic {
|
||||
music: SecretmusicMusic[];
|
||||
}
|
||||
|
||||
export interface SecretmusicMusic {
|
||||
musicid: number;
|
||||
seq: number;
|
||||
kind: number;
|
||||
}
|
||||
|
||||
export interface Sessiondata {
|
||||
date_ms: number;
|
||||
member: { [key: string]: Member };
|
||||
}
|
||||
|
||||
export interface Member {
|
||||
did: number;
|
||||
}
|
||||
|
||||
export interface Skilldata {
|
||||
skill: number;
|
||||
all_skill: number;
|
||||
exist: number[];
|
||||
new: number[];
|
||||
}
|
||||
|
||||
export interface Skindata {
|
||||
skin: number[];
|
||||
}
|
||||
|
||||
export interface ThanksMedal {
|
||||
medal: number;
|
||||
granted_total_medal: number;
|
||||
}
|
||||
|
||||
export interface TitleParts {
|
||||
}
|
||||
|
||||
export interface Tutorial {
|
||||
progress: number;
|
||||
disp_state: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
import { Stage } from "./data.js";
|
||||
|
||||
export interface GameSave {
|
||||
// all s32
|
||||
playInfo: Record<string, number>;
|
||||
rewardStatus: number[];
|
||||
customData: {
|
||||
playStyle: number[];
|
||||
custom: number[];
|
||||
};
|
||||
skill: number;
|
||||
favoriteMusic: {
|
||||
list1: number[];
|
||||
list2: number[];
|
||||
list3: number[];
|
||||
};
|
||||
information: number[];
|
||||
maxRecord: Record<string, number>;
|
||||
diffRecord: Record<string, number | number[]>;
|
||||
}
|
||||
|
||||
export interface PlayerData {
|
||||
_id: string;
|
||||
gameSave: {
|
||||
[type: string]: GameSave
|
||||
};
|
||||
secretMusics: number[];
|
||||
}
|
||||
|
||||
export interface MusicData {
|
||||
player: string;
|
||||
musicId: number;
|
||||
|
||||
seqs: Record<number, {
|
||||
clear: boolean;
|
||||
fullcombo: boolean;
|
||||
excellent: boolean;
|
||||
skill: number;
|
||||
perc: number;
|
||||
rank: number;
|
||||
meter: bigint;
|
||||
meterProgress: number;
|
||||
bestRecord: ObjectId;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PlayRecord extends Stage {
|
||||
player: string;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './data.js';
|
||||
export * from './db.js';
|
||||
+43
-12
@@ -2,6 +2,7 @@ import { to_bin, to_xml } from "@geekidos/kbinxml";
|
||||
import { XMLParser } from "fast-xml-parser";
|
||||
import _ from "lodash";
|
||||
import { Serializable } from "./kxml-value.js";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
export const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
@@ -17,6 +18,17 @@ export const parser = new XMLParser({
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: maybe have a better way to workaround this.
|
||||
const ALWAYS_BIGINT_FOR_64BIT_NUMBER = false;
|
||||
function process64BitInteger(v: string) {
|
||||
const bi = BigInt(v);
|
||||
if (ALWAYS_BIGINT_FOR_64BIT_NUMBER || bi < Number.MIN_SAFE_INTEGER || bi > Number.MAX_SAFE_INTEGER) {
|
||||
return bi;
|
||||
}
|
||||
|
||||
return Number(bi);
|
||||
}
|
||||
|
||||
function parseValue(node: { $__type: string; $__count?: unknown }): any {
|
||||
const isArray = '$__count' in node;
|
||||
|
||||
@@ -28,8 +40,6 @@ function parseValue(node: { $__type: string; $__count?: unknown }): any {
|
||||
'u16',
|
||||
's32',
|
||||
'u32',
|
||||
's64',
|
||||
'u64',
|
||||
].includes(node.$__type)
|
||||
) {
|
||||
if (isArray) {
|
||||
@@ -39,6 +49,20 @@ function parseValue(node: { $__type: string; $__count?: unknown }): any {
|
||||
return parseInt(node['__value']);
|
||||
}
|
||||
|
||||
// javascript Number can't support 64bit data
|
||||
if (
|
||||
[
|
||||
's64',
|
||||
'u64',
|
||||
].includes(node.$__type)
|
||||
) {
|
||||
if (isArray) {
|
||||
return node['__value'].split(' ').map(process64BitInteger);
|
||||
}
|
||||
|
||||
return process64BitInteger(node['__value']);
|
||||
}
|
||||
|
||||
if (['float', 'double'].includes(node.$__type)) {
|
||||
if (isArray) {
|
||||
return node['__value'].split(' ').map((v) => parseFloat(v));
|
||||
@@ -60,7 +84,7 @@ function parseValue(node: { $__type: string; $__count?: unknown }): any {
|
||||
}
|
||||
|
||||
if (node.$__type == 'time') {
|
||||
return new Date(parseInt(node['__value']) * 1000);
|
||||
return new Date(parseInt(node['__value']));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -159,7 +183,7 @@ function serializeValue(value: any, type: string): string {
|
||||
|
||||
if (type == 'time') {
|
||||
if (value instanceof Date) {
|
||||
return Math.floor(value.valueOf() / 1000).toString();
|
||||
return Math.floor(value.valueOf()).toString();
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
@@ -192,7 +216,7 @@ function serializeObject(obj: Serializable, name: string, linePrefix: string = '
|
||||
}
|
||||
|
||||
// the kbinxml library had issue when serializing non-ordered stuff
|
||||
attrs.sort((a, b) => a[0] < b[0] ? 1 : (a[0] > b[0] ? -1 : 0));
|
||||
// attrs.sort((a, b) => a[0] < b[0] ? 1 : (a[0] > b[0] ? -1 : 0));
|
||||
|
||||
for (const attr of attrs) {
|
||||
if (attr[1] === undefined)
|
||||
@@ -201,9 +225,9 @@ function serializeObject(obj: Serializable, name: string, linePrefix: string = '
|
||||
output += ` ${attr[0]}="${_.escape(attr[1])}"`;
|
||||
}
|
||||
|
||||
output += '>';
|
||||
|
||||
if (value) {
|
||||
output += '>';
|
||||
|
||||
if (value[1] === undefined) {
|
||||
return '';
|
||||
}
|
||||
@@ -220,11 +244,11 @@ function serializeObject(obj: Serializable, name: string, linePrefix: string = '
|
||||
.join(' ');
|
||||
} else {
|
||||
const elements = entries
|
||||
.filter(v => !v[0].startsWith('$'))
|
||||
.sort((a, b) => a[0] < b[0] ? 1 : (a[0] > b[0] ? -1 : 0));
|
||||
.filter(v => !v[0].startsWith('$'));
|
||||
// .sort((a, b) => a[0] < b[0] ? 1 : (a[0] > b[0] ? -1 : 0));
|
||||
|
||||
if (elements.length) {
|
||||
output += '\n';
|
||||
output += '>\n';
|
||||
|
||||
for (const element of elements) {
|
||||
if (element[1] instanceof Array) {
|
||||
@@ -238,6 +262,8 @@ function serializeObject(obj: Serializable, name: string, linePrefix: string = '
|
||||
}
|
||||
|
||||
output += linePrefix;
|
||||
} else {
|
||||
return output + ' />\n';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,11 +271,16 @@ function serializeObject(obj: Serializable, name: string, linePrefix: string = '
|
||||
}
|
||||
|
||||
export function toKBinXml(topName: string, obj: Serializable, encoding: 'UTF-8' | 'SHIFT_JIS' = 'UTF-8', dumpXml: boolean = false) {
|
||||
const xml = `<?xml version="1.0" encoding="${encoding}"?>` + serializeObject(obj, topName);
|
||||
const xml = `<?xml version="1.0" encoding="${encoding}"?>\n` + serializeObject(obj, topName);
|
||||
|
||||
if (dumpXml) {
|
||||
console.log(xml);
|
||||
}
|
||||
|
||||
return to_bin(xml);
|
||||
const bin = to_bin(xml);
|
||||
if (dumpXml) {
|
||||
writeFileSync('dump.bin', Buffer.from(bin.data));
|
||||
}
|
||||
|
||||
return bin;
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ export const v = {
|
||||
u16: vg<'u16', number>('u16'),
|
||||
s32: vg<'s32', number>('s32'),
|
||||
u32: vg<'u32', number>('u32'),
|
||||
s64: vg<'s64', number>('s64'),
|
||||
u64: vg<'u64', number>('u64'),
|
||||
s64: vg<'s64', bigint | number>('s64'),
|
||||
u64: vg<'u64', bigint | number>('u64'),
|
||||
float: vg<'float', number>('float'),
|
||||
double: vg<'double', number>('double'),
|
||||
b: vg<'b', boolean>('b'),
|
||||
|
||||
Reference in New Issue
Block a user