Merge pull request #3 from laochan-eacnet/gitadora

Add Gitadora Support
This commit is contained in:
GEEKiDoS
2025-05-01 14:44:52 +08:00
committed by GitHub
13 changed files with 73894 additions and 16 deletions
+72780
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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),
+110
View File
@@ -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,
}
}
}
}
};
}
}
+289
View File
@@ -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),
}
}
},
},
}
}
}
+84
View File
@@ -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(''),
}];
+2 -1
View File
@@ -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);
+2
View File
@@ -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;
+208
View File
@@ -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 });
}
}
+321
View File
@@ -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;
}
+50
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
export * from './data.js';
export * from './db.js';
+43 -12
View File
@@ -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;
}
+2 -2
View File
@@ -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'),