diff --git a/src/controllers/gitadora/index.ts b/src/controllers/gitadora/index.ts index e7bd45c..01902bd 100644 --- a/src/controllers/gitadora/index.ts +++ b/src/controllers/gitadora/index.ts @@ -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() diff --git a/src/controllers/gitadora/user.ts b/src/controllers/gitadora/user.ts index bd747c3..6be6f62 100644 --- a/src/controllers/gitadora/user.ts +++ b/src/controllers/gitadora/user.ts @@ -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 { + 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 { + 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 { + 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), + } + } + }, + }, + } } } diff --git a/src/services/gitadora/user.ts b/src/services/gitadora/user.ts new file mode 100644 index 0000000..65ab7f1 --- /dev/null +++ b/src/services/gitadora/user.ts @@ -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('gitadora_play_data'); + } + + musicDataCol(type: 'gf' | 'dm') { + return this.db.collection('gitadora_' + type + '_music_data'); + } + + playRecordCol(type: 'gf' | 'dm') { + return this.db.collection('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 { + 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): Promise> { + return this.playerDataCol.updateOne({ _id: token }, { + $set: data, + }, { upsert: true }); + } + + findMusicDatas(token: string, type: 'gf' | 'dm'): Promise { + return this.musicDataCol(type).find({ player: token }, { useBigInt64: true }).toArray(); + } + + async updateMusicData(token: string, stage: Stage, type: 'gf' | 'dm'): Promise { + // 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 }); + } +} diff --git a/src/types/gitadora/data.ts b/src/types/gitadora/data.ts index 5692da5..bdd0cff 100644 --- a/src/types/gitadora/data.ts +++ b/src/types/gitadora/data.ts @@ -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; +} + diff --git a/src/types/gitadora/db.ts b/src/types/gitadora/db.ts index 235176e..d15ae55 100644 --- a/src/types/gitadora/db.ts +++ b/src/types/gitadora/db.ts @@ -1,62 +1,47 @@ -import { Long } from "mongodb"; +import { ObjectId } from "mongodb"; +import { Stage } from "./data.js"; + +export interface GameSave { + // all s32 + playInfo: Record; + rewardStatus: number[]; + customData: { + playStyle: number[]; + custom: number[]; + }; + skill: number; + favoriteMusic: { + list1: number[]; + list2: number[]; + list3: number[]; + }; + information: number[]; + maxRecord: Record; + diffRecord: Record; +} export interface PlayerData { _id: string; - gameSave: Record<'gf' | 'dm', { - playCount: number; - playTime: number; - playTerm: number; - // all s32 - playInfo: Record; - 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; - diffRecord: Record; - }>; + 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; +} + +export interface PlayRecord extends Stage { + player: string; } diff --git a/src/utils/kbinxml.ts b/src/utils/kbinxml.ts index 702f33d..6f1464a 100644 --- a/src/utils/kbinxml.ts +++ b/src/utils/kbinxml.ts @@ -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 {