gitadora done

This commit is contained in:
GEEKiDoS
2025-04-18 16:08:23 +08:00
parent 0b8c544dcc
commit 22820d5040
6 changed files with 768 additions and 58 deletions
+6 -2
View File
@@ -1,16 +1,20 @@
import { singleton } from "tsyringe";
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() {
constructor(
@inject(UserService) private readonly userService: UserService,
) {
super();
this.userService;
}
@generic()
+218 -2
View File
@@ -1,8 +1,224 @@
import { UserService } from "../../services/gitadora/user.js";
import { Context } from "../../types.js";
import { Serializable } from "../../utils/kxml-value.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 {
async knst_cardutil_check(ctx: Context): Promise<Serializable> {
userService: UserService;
knst_cardutil_check(): 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,
player: {
$no: 1,
$state: 2,
}
},
},
};
}
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');
}
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_rq_key'),
player_info: {
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),
},
playinfo: Object.fromEntries(
Object.entries(save).map(kv => [kv[0], v.s32(kv[1])])
),
reward: {
status: v.s32(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: {},
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.maxRecord).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.maxRecord).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);
for (const seq in m.seqs) {
const rec = m.seqs[seq];
if (rec.score > bestScore) {
bestScore = rec.score;
bestSeq = +seq;
}
mdata[+seq] = rec.clear ? rec.perc : -1;
mdata[8 + seq] = rec.rank;
meter[+seq] = rec.meter;
meterProg[+seq] = rec.meterProgress;
}
return {
$musicid: m.musicId,
flag: v.u16([0, 0, 0, 0]),
sdata: v.s16([bestSeq, bestScore]),
mdata: v.s16(mdata),
meter: v.u64(meter),
meter_prog: v.s16(meterProg),
}
}),
},
finish: v.bool(true),
},
},
},
};
}
async knst_gameend_regist(ctx: Context): Promise<Serializable> {
const type = this.getGameType(ctx);
const player = ctx.body.player as Player;
await Promise.all([
this.userService.updateMusicData(ctx.token, ctx.body.player.stage, type),
this.userService.upsertPlayer(ctx.token, {
[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),
}
}
},
},
}
}
}
+206
View File
@@ -0,0 +1,206 @@
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: {}
} 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,
} as MusicData)(),
this.playRecordCol(type).insertOne({
player: token,
...stage
}, { useBigInt64: true }),
]);
if (stage.seq in data.seqs) {
data.seqs[stage.seq] = {
fullcombo: stage.fullcombo,
excellent: stage.excellent,
clear: stage.clear,
score: stage.score,
perc: stage.perc,
rank: stage.rank,
meter: stage.meter,
meterProgress: stage.meter_prog,
bestRecord: result.insertedId,
}
} else {
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.score = Math.max(seqData.score, stage.score);
if (stage.meter_prog > seqData.meterProgress) {
seqData.meterProgress = stage.meter_prog;
seqData.meter = stage.meter;
}
}
await this.musicDataCol(type).updateOne({ player: token, musicId: stage.musicid }, {
$set: data,
}, { useBigInt64: true });
}
}
+299
View File
@@ -74,3 +74,302 @@ export interface MdbData {
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: Rec;
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 Rec {
max: Max;
diff: Diff;
}
export interface Diff {
diff_100_nr: number;
diff_150_nr: number;
diff_200_nr: number;
diff_250_nr: number;
diff_300_nr: number;
diff_350_nr: number;
diff_400_nr: number;
diff_450_nr: number;
diff_500_nr: number;
diff_550_nr: number;
diff_600_nr: number;
diff_650_nr: number;
diff_700_nr: number;
diff_750_nr: number;
diff_800_nr: number;
diff_850_nr: number;
diff_900_nr: number;
diff_950_nr: number;
diff_100_clear: number[];
diff_150_clear: number[];
diff_200_clear: number[];
diff_250_clear: number[];
diff_300_clear: number[];
diff_350_clear: number[];
diff_400_clear: number[];
diff_450_clear: number[];
diff_500_clear: number[];
diff_550_clear: number[];
diff_600_clear: number[];
diff_650_clear: number[];
diff_700_clear: number[];
diff_750_clear: number[];
diff_800_clear: number[];
diff_850_clear: number[];
diff_900_clear: number[];
diff_950_clear: number[];
}
export interface Max {
skill: number;
all_skill: number;
clear_diff: number;
full_diff: number;
exce_diff: number;
clear_music_num: number;
full_music_num: number;
exce_music_num: number;
clear_seq_num: number;
classic_all_skill: 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;
}
+37 -52
View File
@@ -1,62 +1,47 @@
import { Long } from "mongodb";
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: Record<'gf' | 'dm', {
playCount: number;
playTime: number;
playTerm: number;
// all s32
playInfo: Record<string, number>;
tutorial: {
progress: number;
displayState: 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>;
}>;
gameSave: Record<'gf' | 'dm', GameSave>;
}
export interface MusicData {
type: 'gf' | 'dm';
player: string;
musicId: number;
clearStatus: number;
rank: number;
perfect: number;
great: number;
good: number;
ok: number;
miss: number;
maxCombo: number;
score: number;
flags: number;
point: number;
percent: number;
meter: Long;
phrase: {
count: number;
addr: number[];
type: number[];
status: number[];
endAddr: number;
comboAerc: number;
}
seqs: Record<number, {
clear: boolean;
fullcombo: boolean;
excellent: boolean;
score: number;
perc: number;
rank: number;
meter: bigint;
meterProgress: number;
bestRecord: ObjectId;
}>;
}
export interface PlayRecord extends Stage {
player: string;
}
+2 -2
View File
@@ -18,7 +18,7 @@ export const parser = new XMLParser({
});
// TODO: maybe have a better way to workaround this.
const ALWAYS_BIGINT_FOR_64BIT_NUMBER = true;
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) {
@@ -83,7 +83,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 {