From ad677b4c2b0684ded99385eff1e71fdeea3b2632 Mon Sep 17 00:00:00 2001 From: cracrayol Date: Fri, 9 Apr 2021 23:08:37 +0200 Subject: [PATCH] Pop'n Music plugin v2.0.0 * Big rewrite/reorganization of the code * Add support for Tune Street, fantasia, Sunny Park, Lapistoria * Add automatic convertion from plugin v1.x data to v2.x * Enable/disable score sharing between versions * Various fixes --- popn@asphyxia/README.md | 28 +- popn@asphyxia/handler/common.ts | 324 ------------ popn@asphyxia/handler/eclale.ts | 492 ++++++++++++++++++ popn@asphyxia/handler/fantasia.ts | 276 ++++++++++ popn@asphyxia/handler/lapistoria.ts | 441 ++++++++++++++++ popn@asphyxia/handler/player.ts | 519 ------------------- popn@asphyxia/handler/sunny.ts | 321 ++++++++++++ popn@asphyxia/handler/tunestreet.ts | 273 ++++++++++ popn@asphyxia/handler/usaneko.ts | 719 +++++++++++++++++++++++++++ popn@asphyxia/handler/utils.ts | 222 ++++++++- popn@asphyxia/handler/webui.ts | 3 +- popn@asphyxia/index.ts | 64 ++- popn@asphyxia/models/achievements.ts | 93 ++++ popn@asphyxia/models/common.ts | 42 ++ popn@asphyxia/models/profile.ts | 40 -- popn@asphyxia/models/scores.ts | 13 - 16 files changed, 2937 insertions(+), 933 deletions(-) delete mode 100644 popn@asphyxia/handler/common.ts create mode 100644 popn@asphyxia/handler/eclale.ts create mode 100644 popn@asphyxia/handler/fantasia.ts create mode 100644 popn@asphyxia/handler/lapistoria.ts delete mode 100644 popn@asphyxia/handler/player.ts create mode 100644 popn@asphyxia/handler/sunny.ts create mode 100644 popn@asphyxia/handler/tunestreet.ts create mode 100644 popn@asphyxia/handler/usaneko.ts create mode 100644 popn@asphyxia/models/achievements.ts create mode 100644 popn@asphyxia/models/common.ts delete mode 100644 popn@asphyxia/models/profile.ts delete mode 100644 popn@asphyxia/models/scores.ts diff --git a/popn@asphyxia/README.md b/popn@asphyxia/README.md index 35ca291..735fd01 100644 --- a/popn@asphyxia/README.md +++ b/popn@asphyxia/README.md @@ -1,13 +1,25 @@ # Pop'n Music -Plugin Version: **v1.2.0** +Plugin Version: **v2.0.0** ## Supported Versions -- pop'n music 23 Eclale -- pop'n music 24 Usagi to Neko to Shōnen no Yume -- pop'n music 25 peace +- pop'n music 19 Tune Street +- pop'n music 20 fantasia +- pop'n music Sunny Park +- pop'n music Lapistoria +- pop'n music éclale +- pop'n music Usagi to Neko to Shōnen no Yume +- pop'n music peace ## Changelog + +### 2.0.0 +* Big rewrite/reorganization of the code +* Add support for Tune Street, fantasia, Sunny Park, Lapistoria +* Add automatic convertion from plugin v1.x data to v2.x +* Enable/disable score sharing between versions +* Various fixes + #### 1.2.0 * You can change your profile name * You can enable/disable the pop'n 25 event archive event @@ -25,4 +37,10 @@ To import data, you have to : * Create a backup of your savedata.db file (in case something goes wrong). * In the web UI of Asphyxia, go to POPN -> Profile and click detail on your profile * Put the content of your non-core asphyxia popn music files in the text fields (pop.json and popn_scores.json) and click Import. -* Data is imported. Run the game, insert your card and your scores are available. \ No newline at end of file +* Data is imported. Run the game, insert your card and your scores are available. + +## Known limitations +* Tune Street : It will not report your profile name in-game +* Tune Street : No Town Mode +* No rival support implemented +* Some stats are not implemented (like daily stats, most played music) \ No newline at end of file diff --git a/popn@asphyxia/handler/common.ts b/popn@asphyxia/handler/common.ts deleted file mode 100644 index 61068a5..0000000 --- a/popn@asphyxia/handler/common.ts +++ /dev/null @@ -1,324 +0,0 @@ -import { getVersion } from './utils'; - -interface Phase { - id: number; - p: number; -} - -const PHASE23 : Phase[] = [ - { id: 0, p: 16 }, - { id: 1, p: 3 }, - { id: 2, p: 1 }, - { id: 3, p: 2 }, - { id: 4, p: 1 }, - { id: 5, p: 2 }, - { id: 6, p: 1 }, - { id: 7, p: 4 }, - { id: 8, p: 3 }, - { id: 9, p: 4 }, - { id: 10, p: 4 }, - { id: 11, p: 1 }, - { id: 12, p: 1 }, - { id: 13, p: 4 }, -]; - -const PHASE24 : Phase[] = [ - { id: 0, p: 11 }, - { id: 1, p: 2 }, - { id: 2, p: 2 }, - { id: 3, p: 4 }, - { id: 4, p: 1 }, -// { id: 5, p: 1 }, // Disable Net Taisen - { id: 6, p: 1 }, - { id: 7, p: 1 }, - { id: 8, p: 2 }, - { id: 9, p: 2 }, - { id: 10, p: 15 }, - { id: 11, p: 1 }, - { id: 12, p: 2 }, - { id: 13, p: 1 }, -]; - -const PHASE25 : Phase[] = [ - { id: 0, p: 23 }, - { id: 1, p: 4 }, - { id: 2, p: 2 }, - { id: 3, p: 4 }, - { id: 4, p: 1 }, -// { id: 5, p: 1 }, // Disable Net Taisen - { id: 6, p: 1 }, - { id: 7, p: 1 }, - { id: 8, p: 2 }, - { id: 9, p: 2 }, - { id: 10, p: 30 }, - { id: 11, p: 1 }, - { id: 12, p: 2 }, - { id: 13, p: 1 }, - { id: 14, p: 39 }, - { id: 15, p: 2 }, - { id: 16, p: 3 }, - { id: 17, p: 8 }, - { id: 18, p: 1 }, - { id: 19, p: 1 }, - { id: 20, p: 13 }, -// { id: 21, p: 20 }, // pop'n event archive - { id: 22, p: 2 }, - { id: 23, p: 1 }, - { id: 24, p: 1 }, -]; - -export const getInfo = (req: EamuseInfo) => { - const version = getVersion(req); - - if (version == 'v23') { - return getInfo23(); - } else if (version == 'v24') { - if(req.model == 'M39:J:A:A:2020092800') { - let phase = [...PHASE25]; - phase.push({ id: 21, p: U.GetConfig("enable_25_event") ? 20 : 0 }) - return getInfo24(phase); - } else { - return getInfo24(PHASE24); - } - } -} - -const getInfo23 = () => { - const result: any = { - phase: [], - area: [], - goods: [], - }; - - for (const phase of PHASE23) { - result.phase.push({ - event_id: K.ITEM('s16', phase.id), - phase: K.ITEM('s16', phase.p), - }); - } - - for (let i = 1; i <= 100; ++i) { - result.area.push({ - area_id: K.ITEM('s16', i), - end_date: K.ITEM('u64', BigInt(0)), - medal_id: K.ITEM('s16', i), - is_limit: K.ITEM('bool', 0), - }); - } - - for (let i = 1; i <= 420; ++i) { - result.goods.push({ - goods_id: K.ITEM('s16', i), - price: K.ITEM('s32', i <= 80 ? 60 : 100), - goods_type: K.ITEM('s16', 0), - }); - } - - return result; -}; - -const getInfo24 = (phaseData : Phase[]) => { - const result: any = { - phase: [], - goods: [], - }; - - for (const phase of phaseData) { - result.phase.push({ - event_id: K.ITEM('s16', phase.id), - phase: K.ITEM('s16', phase.p), - }); - } - - for (let i = 3; i <= 96; ++i) { - result.goods.push({ - item_id: K.ITEM('s32', i), - item_type: K.ITEM('s16', 3), - price: K.ITEM('s32', 60), - goods_type: K.ITEM('s16', 0), - }); - } - - return result; -}; - -export const M39_EXTRA_DATA: { - [ver: string]: { - [field: string]: { - path: string; - type: string; - default: any; - isArray?: true; - }; - }; -} = { - v23: { - tutorial: { type: 's8', path: 'account', default: 0 }, - area_id: { type: 's16', path: 'account', default: 0 }, - lumina: { type: 's16', path: 'account', default: 300 }, - medal_set: { type: 's16', path: 'account', default: [0, 0, 0, 0], isArray: true }, - read_news: { type: 's16', path: 'account', default: 0 }, - staff: { type: 's8', path: 'account', default: 0 }, - item_type: { type: 's16', path: 'account', default: 0 }, - item_id: { type: 's16', path: 'account', default: 0 }, - is_conv: { type: 's8', path: 'account', default: 0 }, - active_fr_num: { type: 'u8', path: 'account', default: 0 }, - nice: { type: 's16', path: 'account', default: Array(30).fill(-1), isArray: true }, - favorite_chara: { type: 's16', path: 'account', default: Array(20).fill(-1), isArray: true }, - special_area: { type: 's16', path: 'account', default: Array(8).fill(0), isArray: true }, - chocolate_charalist: { - type: 's16', - path: 'account', - default: Array(5).fill(-1), - isArray: true, - }, - teacher_setting: { type: 's16', path: 'account', default: Array(10).fill(0), isArray: true }, - license_data: { type: 's16', path: 'account', default: [-1, -1], isArray: true }, - welcom_pack: { type: 'bool', path: 'account', default: 1 }, - meteor_flg: { type: 'bool', path: 'account', default: 1 }, - - hispeed: { type: 's16', path: 'option', default: 0 }, - popkun: { type: 'u8', path: 'option', default: 0 }, - hidden: { type: 'bool', path: 'option', default: 0 }, - hidden_rate: { type: 's16', path: 'option', default: 0 }, - sudden: { type: 'bool', path: 'option', default: 0 }, - sudden_rate: { type: 's16', path: 'option', default: 0 }, - randmir: { type: 's8', path: 'option', default: 0 }, - gauge_type: { type: 's8', path: 'option', default: 0 }, - ojama_0: { type: 'u8', path: 'option', default: 0 }, - ojama_1: { type: 'u8', path: 'option', default: 0 }, - forever_0: { type: 'bool', path: 'option', default: 0 }, - forever_1: { type: 'bool', path: 'option', default: 0 }, - full_setting: { type: 'bool', path: 'option', default: 0 }, - judge: { type: 'u8', path: 'option', default: 0 }, - - ep: { type: 'u16', path: 'info', default: 0 }, - - effect_left: { type: 'u16', path: 'customize', default: 0 }, - effect_center: { type: 'u16', path: 'customize', default: 0 }, - effect_right: { type: 'u16', path: 'customize', default: 0 }, - hukidashi: { type: 'u16', path: 'customize', default: 0 }, - comment_1: { type: 'u16', path: 'customize', default: 0 }, - comment_2: { type: 'u16', path: 'customize', default: 0 }, - - mode: { type: 'u8', path: 'config', default: 0 }, - chara: { type: 's16', path: 'config', default: -1 }, - music: { type: 's16', path: 'config', default: -1 }, - sheet: { type: 'u8', path: 'config', default: 0 }, - category: { type: 's8', path: 'config', default: -1 }, - sub_category: { type: 's8', path: 'config', default: -1 }, - chara_category: { type: 's8', path: 'config', default: -1 }, - course_id: { type: 's16', path: 'config', default: 0 }, - course_folder: { type: 's8', path: 'config', default: 0 }, - ms_banner_disp: { type: 's8', path: 'config', default: 0 }, - ms_down_info: { type: 's8', path: 'config', default: 0 }, - ms_side_info: { type: 's8', path: 'config', default: 0 }, - ms_raise_type: { type: 's8', path: 'config', default: 0 }, - ms_rnd_type: { type: 's8', path: 'config', default: 0 }, - - enemy_medal: { type: 's16', path: 'event', default: 0 }, - hp: { type: 's16', path: 'event', default: 0 }, - - valid: { type: 's8', path: 'custom_cate', default: 0 }, - lv_min: { type: 's8', path: 'custom_cate', default: -1 }, - lv_max: { type: 's8', path: 'custom_cate', default: -1 }, - medal_min: { type: 's8', path: 'custom_cate', default: -1 }, - medal_max: { type: 's8', path: 'custom_cate', default: -1 }, - friend_no: { type: 's8', path: 'custom_cate', default: -1 }, - score_flg: { type: 's8', path: 'custom_cate', default: -1 }, - },v24: { - enemy_medal: { type: 's16', path: 'event', default: 0 }, - hp: { type: 's16', path: 'event', default: 0 }, - - tutorial: { type: 's16', path: 'account', default: -1 }, - area_id: { type: 's16', path: 'account', default: 51 }, - lumina: { type: 's16', path: 'account', default: 0 }, - medal_set: { type: 's16', path: 'account', default: [0, 0], isArray: true }, - read_news: { type: 's16', path: 'account', default: 0 }, - staff: { type: 's8', path: 'account', default: 0 }, - is_conv: { type: 's8', path: 'account', default: 0 }, - item_type: { type: 's16', path: 'account', default: 0 }, - item_id: { type: 's16', path: 'account', default: 0 }, - license_data: { type: 's16', path: 'account', default: Array(10).fill(-1), isArray: true }, - active_fr_num: { type: 'u8', path: 'account', default: 0 }, - nice: { type: 's16', path: 'account', default: Array(30).fill(-1), isArray: true }, - favorite_chara: { type: 's16', path: 'account', default: Array(10).fill(-1), isArray: true }, - special_area: { type: 's16', path: 'account', default: Array(8).fill(-1), isArray: true }, - chocolate_charalist: { - type: 's16', - path: 'account', - default: Array(5).fill(-1), - isArray: true, - }, - chocolate_sp_chara: { type: 's32', path: 'account', default: 0 }, - chocolate_pass_cnt: { type: 's32', path: 'account', default: 0 }, - chocolate_hon_cnt: { type: 's32', path: 'account', default: 0 }, - chocolate_giri_cnt: { type: 's32', path: 'account', default: 0 }, - chocolate_kokyu_cnt: { type: 's32', path: 'account', default: 0 }, - teacher_setting: { type: 's16', path: 'account', default: Array(10).fill(-1), isArray: true }, - welcom_pack: { type: 'bool', path: 'account', default: 0 }, - meteor_flg: { type: 'bool', path: 'account', default: 0 }, - use_navi: { type: 's16', path: 'account', default: 0 }, - ranking_node: { type: 's32', path: 'account', default: 0 }, - chara_ranking_kind_id: { type: 's32', path: 'account', default: 0 }, - navi_evolution_flg: { type: 's8', path: 'account', default: 0 }, - ranking_news_last_no: { type: 's32', path: 'account', default: 0 }, - power_point: { type: 's32', path: 'account', default: 0 }, - player_point: { type: 's32', path: 'account', default: 0 }, - power_point_list: { type: 's32', path: 'account', default: [0], isArray: true }, - - mode: { type: 'u8', path: 'config', default: 0 }, - chara: { type: 's16', path: 'config', default: 0 }, - music: { type: 's16', path: 'config', default: 0 }, - sheet: { type: 'u8', path: 'config', default: 0 }, - category: { type: 's8', path: 'config', default: 0 }, - sub_category: { type: 's8', path: 'config', default: 0 }, - chara_category: { type: 's8', path: 'config', default: 0 }, // check - story_id: { type: 's16', path: 'config', default: 0 }, - ms_banner_disp: { type: 's8', path: 'config', default: 0 }, - ms_down_info: { type: 's8', path: 'config', default: 0 }, - ms_side_info: { type: 's8', path: 'config', default: 0 }, - ms_raise_type: { type: 's8', path: 'config', default: 0 }, - ms_rnd_type: { type: 's8', path: 'config', default: 0 }, - banner_sort: { type: 's8', path: 'config', default: 0 }, - course_id: { type: 's16', path: 'config', default: 0 }, - course_folder: { type: 's8', path: 'config', default: 0 }, - story_folder: { type: 's8', path: 'config', default: 0 }, - - hispeed: { type: 's16', path: 'option', default: 10 }, - popkun: { type: 'u8', path: 'option', default: 0 }, - hidden: { type: 'bool', path: 'option', default: 0 }, - hidden_rate: { type: 's16', path: 'option', default: -1 }, - sudden: { type: 'bool', path: 'option', default: 0 }, - sudden_rate: { type: 's16', path: 'option', default: -1 }, - randmir: { type: 's8', path: 'option', default: 0 }, - gauge_type: { type: 's8', path: 'option', default: 0 }, - ojama_0: { type: 'u8', path: 'option', default: 0 }, - ojama_1: { type: 'u8', path: 'option', default: 0 }, - forever_0: { type: 'bool', path: 'option', default: 0 }, - forever_1: { type: 'bool', path: 'option', default: 0 }, - full_setting: { type: 'bool', path: 'option', default: 0 }, - guide_se: { type: 's8', path: 'option', default: 0 }, - judge: { type: 'u8', path: 'option', default: 0 }, - slow: { type: 's16', path: 'option', default: 0 }, - fast: { type: 's16', path: 'option', default: 0 }, - - valid: { type: 's8', path: 'custom_cate', default: 0 }, - lv_min: { type: 's8', path: 'custom_cate', default: 0 }, - lv_max: { type: 's8', path: 'custom_cate', default: 0 }, - medal_min: { type: 's8', path: 'custom_cate', default: 0 }, - medal_max: { type: 's8', path: 'custom_cate', default: 0 }, - friend_no: { type: 's8', path: 'custom_cate', default: 0 }, - score_flg: { type: 's8', path: 'custom_cate', default: 0 }, - - ep: { type: 'u16', path: 'info', default: 0 }, - ap: { type: 'u16', path: 'info', default: 0 }, - - effect_left: { type: 'u16', path: 'customize', default: 0 }, - effect_center: { type: 'u16', path: 'customize', default: 0 }, - effect_right: { type: 'u16', path: 'customize', default: 0 }, - hukidashi: { type: 'u16', path: 'customize', default: 0 }, - comment_1: { type: 'u16', path: 'customize', default: 0 }, - comment_2: { type: 'u16', path: 'customize', default: 0 }, - }, -}; \ No newline at end of file diff --git a/popn@asphyxia/handler/eclale.ts b/popn@asphyxia/handler/eclale.ts new file mode 100644 index 0000000..deb9ab9 --- /dev/null +++ b/popn@asphyxia/handler/eclale.ts @@ -0,0 +1,492 @@ +import { AchievementsEclale } from "../models/achievements"; +import { ExtraData, Params, Phase } from "../models/common"; +import * as utils from "./utils"; + +export const setRoutes = () => { + R.Route(`info23.common`, getInfo); + R.Route(`player23.new`, newPlayer); + R.Route(`player23.read`, read); + R.Route(`player23.start`, start); + R.Route(`player23.buy`, buy); + R.Route(`player23.read_score`, readScore); + R.Route(`player23.write_music`, writeScore); + R.Route(`player23.write`, write); +} + +const getInfoCommon = (req: EamuseInfo) => { + const result: any = { + phase: [], + area: [], + goods: [], + }; + + // Phase + for (const elt of phase) { + result.phase.push({ + event_id: K.ITEM('s16', elt.id), + phase: K.ITEM('s16', elt.p), + }); + } + + // Area + for (let i = 1; i <= 100; ++i) { + result.area.push({ + area_id: K.ITEM('s16', i), + end_date: K.ITEM('u64', BigInt(0)), + medal_id: K.ITEM('s16', i), + is_limit: K.ITEM('bool', 0), + }); + } + + // Goods + for (let i = 1; i <= 420; ++i) { + let price = 150; + + if (i <= 80) { + price = 60; + } else if (i <= 120) { + price = 250; + } else if (i <= 142) { + price = 500; + } else if (i <= 300) { + price = 100; + } + + result.goods.push({ + goods_id: K.ITEM('s16', i), + price: K.ITEM('s32', price), + goods_type: K.ITEM('s16', 0), + }); + } + + // TODO : Course ranking + // TODO : Most popular characters + // TODO : Most popular music + + return result; +} + +const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + return send.object(getInfoCommon(req)); +} + +const start = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const result = { + play_id: K.ITEM('s32', 1), + ...getInfoCommon(req), + }; + await send.object(result); +}; + +/** + * Create a new profile and send it. + */ +const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const name = $(data).str('name'); + + send.object(await getProfile(refid, name)); +}; + +/** + * Read a profile and send it. + */ +const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + send.object(await getProfile(refid)); +}; + +const buy = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const type = $(data).number('type', -1); + const id = $(data).number('id', -1); + const param = $(data).number('param', 0); + const price = $(data).number('price', 0); + const lumina = $(data).number('lumina', 0); + + if (type < 0 || id < 0) { + return send.deny(); + } + + if (lumina >= price) { + const params = await utils.readParams(refid, version); + params.params.player_point = lumina - price; + await utils.writeParams(refid, version, params); + + const achievements = await utils.readAchievements(refid, version, defaultAchievements); + achievements.items[`${type}:${id}`] = param; + await utils.writeAchievements(refid, version, achievements); + } + send.success(); +}; + +const readScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const scoresData = await utils.readScores(refid, version); + const result: any = { + music: [], + }; + + for (const key in scoresData.scores) { + const keyData = key.split(':'); + const score = scoresData.scores[key]; + const music = parseInt(keyData[0], 10); + const sheet = parseInt(keyData[1], 10); + + if (music > GAME_MAX_MUSIC_ID) { + continue; + } + if ([0, 1, 2, 3].indexOf(sheet) == -1) { + continue; + } + + result.music.push({ + music_num: K.ITEM('s16', music), + sheet_num: K.ITEM('u8', sheet), + score: K.ITEM('s32', score.score), + clear_type: K.ITEM('u8', { + 100: 1, + 200: 2, + 300: 3, + 400: 4, + 500: 5, + 600: 6, + 700: 7, + 800: 8, + 900: 9, + 1000: 10, + 1100: 11, + }[score.clear_type]), + cnt: K.ITEM('s16', score.cnt), + }); + } + + send.object(result); +}; + +const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const music = $(data).number('music_num'); + const sheet = $(data).number('sheet_num'); + const clear_type = { + 1: 100, + 2: 200, + 3: 300, + 4: 400, + 5: 500, + 6: 600, + 7: 700, + 8: 800, + 9: 900, + 10: 1000, + 11: 1100, + }[$(data).number('clearmedal')]; + const score = $(data).number('score'); + + const key = `${music}:${sheet}`; + + const scoresData = await utils.readScores(refid, version, true); + if (!scoresData.scores[key]) { + scoresData.scores[key] = { + score, + cnt: 1, + clear_type + }; + } else { + scoresData.scores[key] = { + score: Math.max(score, scoresData.scores[key].score), + cnt: scoresData.scores[key].cnt + 1, + clear_type: Math.max(clear_type, scoresData.scores[key].clear_type || 0) + }; + } + + utils.writeScores(refid, version, scoresData); + + send.success(); +}; + +/** + * Get/create the profile based on refid + * @param refid the profile refid + * @param name if defined, create/update the profile with the given name + * @returns + */ +const getProfile = async (refid: string, name?: string) => { + const profile = await utils.readProfile(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + let player: any = { + result: K.ITEM('s8', 0), + account: { + name: K.ITEM('str', profile.name), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + staff: K.ITEM('s8', 0), + item_type: K.ITEM('s16', 0), + item_id: K.ITEM('s16', 0), + is_conv: K.ITEM('s8', 0), + meteor_flg: K.ITEM('bool', true), + license_data: K.ARRAY('s16', Array(20).fill(-1)), + + // TODO: replace with real data + total_play_cnt: K.ITEM('s16', 100), + today_play_cnt: K.ITEM('s16', 50), + consecutive_days: K.ITEM('s16', 365), + total_days: K.ITEM('s16', 366), + interval_day: K.ITEM('s16', 1), + my_best: K.ARRAY('s16', Array(10).fill(-1)), + latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), + active_fr_num: K.ITEM('u8', 0), + }, + netvs: { + record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), + dialog: [ + K.ITEM('str', 'dialog#0'), + K.ITEM('str', 'dialog#1'), + K.ITEM('str', 'dialog#2'), + K.ITEM('str', 'dialog#3'), + K.ITEM('str', 'dialog#4'), + K.ITEM('str', 'dialog#5'), + ], + ojama_condition: K.ARRAY('s8', Array(74).fill(0)), + set_ojama: K.ARRAY('s8', [0, 0, 0]), + set_recommend: K.ARRAY('s8', [0, 0, 0]), + netvs_play_cnt: K.ITEM('u32', 0), + }, + custom_cate: { + valid: K.ITEM('s8', 0), + lv_min: K.ITEM('s8', -1), + lv_max: K.ITEM('s8', -1), + medal_min: K.ITEM('s8', -1), + medal_max: K.ITEM('s8', -1), + friend_no: K.ITEM('s8', -1), + score_flg: K.ITEM('s8', -1), + }, + + item: [], + chara_param: [], + medal: [], + }; + + const achievements = await utils.readAchievements(refid, version, defaultAchievements); + + const profileCharas = achievements.charas || {}; + for (const chara_id in profileCharas) { + player.chara_param.push({ + chara_id: K.ITEM('u16', parseInt(chara_id, 10)), + friendship: K.ITEM('u16', profileCharas[chara_id]), + }); + } + + const profileMedals = achievements.medals || {}; + for (const medal_id in profileMedals) { + const medal = profileMedals[medal_id]; + player.medal.push({ + medal_id: K.ITEM('s16', parseInt(medal_id, 10)), + level: K.ITEM('s16', medal.level), + exp: K.ITEM('s32', medal.exp), + set_count: K.ITEM('s32', medal.set_count), + get_count: K.ITEM('s32', medal.get_count), + }); + } + + const profileItems = achievements.items || {}; + for (const key in profileItems) { + const keyData = key.split(':'); + const type = parseInt(keyData[0], 10); + const id = parseInt(keyData[1], 10); + + const item: any = { + type: K.ITEM('u8', type), + id: K.ITEM('u16', id), + param: K.ITEM('u16', profileItems[key]), + is_new: K.ITEM('bool', 0), + }; + + player.item.push(item); + } + + // Add version specific datas + const params = await utils.readParams(refid, version); + utils.addExtraData(player, params, extraData); + + return player; +} + +const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const params = await utils.readParams(refid, version); + const achievements = await utils.readAchievements(refid, version, defaultAchievements); + + utils.getExtraData(data, params, extraData); + + // medals + let medals = _.get(data, 'medal', []); + if (!achievements.medals) { + achievements.medals = {}; + } + + if (!_.isArray(medals)) { + medals = [medals]; + } + + for (const medal of medals) { + const id = $(medal).number('medal_id'); + const level = $(medal).number('level'); + const exp = $(medal).number('exp'); + const set_count = $(medal).number('set_count'); + const get_count = $(medal).number('get_count'); + + achievements.medals[id] = { + level, + exp, + set_count, + get_count, + }; + } + + // items + let items = _.get(data, 'item', []); + if (!achievements.items) { + achievements.items = {}; + } + + if (!_.isArray(items)) { + items = [items]; + } + + for (const item of items) { + const type = $(item).number('type'); + const id = $(item).number('id'); + const param = $(item).number('param'); + + const key = `${type}:${id}`; + + achievements.items[key] = param; + } + + // charas + let charas = _.get(data, 'chara_param', []); + if (!achievements.charas) { + achievements.charas = {}; + } + + if (!_.isArray(charas)) { + charas = [charas]; + } + + for (const chara of charas) { + const id = $(chara).number('chara_id'); + const param = $(chara).number('friendship'); + + achievements.charas[id] = param; + } + + await utils.writeParams(refid, version, params); + await utils.writeAchievements(refid, version, achievements); + + send.success(); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const version: string = 'v23'; +const GAME_MAX_MUSIC_ID = 1550; + +const defaultAchievements: AchievementsEclale = { + collection: 'achievements', + version: 'v23', + medals: {}, + items: {}, + charas: {}, +} + +const phase: Phase[] = [ + { id: 0, p: 16 }, + { id: 1, p: 3 }, + { id: 2, p: 1 }, + { id: 3, p: 2 }, + { id: 4, p: 1 }, + { id: 5, p: 2 }, + { id: 6, p: 1 }, + { id: 7, p: 4 }, + { id: 8, p: 3 }, + { id: 9, p: 4 }, + { id: 10, p: 4 }, + { id: 11, p: 1 }, + { id: 12, p: 1 }, + { id: 13, p: 4 }, +]; +const extraData: ExtraData = { + tutorial: { type: 's8', path: 'account', default: 0 }, + area_id: { type: 's16', path: 'account', default: 0 }, + lumina: { type: 's16', path: 'account', default: 300 }, + read_news: { type: 's16', path: 'account', default: 0 }, + welcom_pack: { type: 'bool', path: 'account', default: 1 }, + medal_set: { type: 's16', path: 'account', default: Array(4).fill(0), isArray: true }, + nice: { type: 's16', path: 'account', default: Array(30).fill(-1), isArray: true }, + favorite_chara: { type: 's16', path: 'account', default: Array(20).fill(-1), isArray: true }, + special_area: { type: 's16', path: 'account', default: Array(8).fill(0), isArray: true }, + chocolate_charalist: { type: 's16', path: 'account', default: Array(5).fill(-1), isArray: true }, + teacher_setting: { type: 's16', path: 'account', default: Array(10).fill(0), isArray: true }, + + hispeed: { type: 's16', path: 'option', default: 0 }, + popkun: { type: 'u8', path: 'option', default: 0 }, + hidden: { type: 'bool', path: 'option', default: 0 }, + hidden_rate: { type: 's16', path: 'option', default: 0 }, + sudden: { type: 'bool', path: 'option', default: 0 }, + sudden_rate: { type: 's16', path: 'option', default: 0 }, + randmir: { type: 's8', path: 'option', default: 0 }, + gauge_type: { type: 's8', path: 'option', default: 0 }, + ojama_0: { type: 'u8', path: 'option', default: 0 }, + ojama_1: { type: 'u8', path: 'option', default: 0 }, + forever_0: { type: 'bool', path: 'option', default: 0 }, + forever_1: { type: 'bool', path: 'option', default: 0 }, + full_setting: { type: 'bool', path: 'option', default: 0 }, + judge: { type: 'u8', path: 'option', default: 0 }, + + ep: { type: 'u16', path: 'info', default: 0 }, + + effect_left: { type: 'u16', path: 'customize', default: 0 }, + effect_center: { type: 'u16', path: 'customize', default: 0 }, + effect_right: { type: 'u16', path: 'customize', default: 0 }, + hukidashi: { type: 'u16', path: 'customize', default: 0 }, + comment_1: { type: 'u16', path: 'customize', default: 0 }, + comment_2: { type: 'u16', path: 'customize', default: 0 }, + + mode: { type: 'u8', path: 'config', default: 0 }, + chara: { type: 's16', path: 'config', default: -1 }, + music: { type: 's16', path: 'config', default: -1 }, + sheet: { type: 'u8', path: 'config', default: 0 }, + category: { type: 's8', path: 'config', default: -1 }, + sub_category: { type: 's8', path: 'config', default: -1 }, + chara_category: { type: 's8', path: 'config', default: -1 }, + course_id: { type: 's16', path: 'config', default: 0 }, + course_folder: { type: 's8', path: 'config', default: 0 }, + ms_banner_disp: { type: 's8', path: 'config', default: 0 }, + ms_down_info: { type: 's8', path: 'config', default: 0 }, + ms_side_info: { type: 's8', path: 'config', default: 0 }, + ms_raise_type: { type: 's8', path: 'config', default: 0 }, + ms_rnd_type: { type: 's8', path: 'config', default: 0 }, + + enemy_medal: { type: 's16', path: 'event', default: 0 }, + hp: { type: 's16', path: 'event', default: 0 }, + + stamp_id: { type: 's16', path: 'stamp', default: 0 }, + cnt: { type: 's16', path: 'stamp', default: 0 }, +} \ No newline at end of file diff --git a/popn@asphyxia/handler/fantasia.ts b/popn@asphyxia/handler/fantasia.ts new file mode 100644 index 0000000..454252e --- /dev/null +++ b/popn@asphyxia/handler/fantasia.ts @@ -0,0 +1,276 @@ +import { ExtraData } from "../models/common"; +import * as utils from "./utils"; + +/** + * Return the current phases of the game. + */ +export const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const result = { + game_phase: K.ITEM('s32', 2), + ir_phase: K.ITEM('s32', 0), + event_phase: K.ITEM('s32', 5), + netvs_phase: K.ITEM('s32', 0), + card_phase: K.ITEM('s32', 6), + illust_phase: K.ITEM('s32', 2), + psp_phase: K.ITEM('s32', 5), + other_phase: K.ITEM('s32', 1), + jubeat_phase: K.ITEM('s32', 1), + public_phase: K.ITEM('s32', 3), + kac_phase: K.ITEM('s32', 2), + local_matching: K.ITEM('s32', 1), + n_matching_sec: K.ITEM('s32', 60), + l_matching_sec: K.ITEM('s32', 60), + is_check_cpu: K.ITEM('s32', 0), + week_no: K.ITEM('s32', 0), + ng_illust: K.ARRAY('s32', Array(10).fill(0)), + sel_ranking: K.ARRAY('s16', Array(10).fill(-1)), + up_ranking: K.ARRAY('s16', Array(10).fill(-1)), + }; + + return send.object(result); +}; + +/** + * Create a new profile and send it. + */ +export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const name = $(data).str('name'); + + send.object(await getProfile(refid, name)); +}; + +/** + * Read a profile and send it. + */ +export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + send.object(await getProfile(refid)); +}; + +/** + * Get/create the profile based on refid and format it + * @param refid the profile refid + * @param name if defined, create/update the profile with the given name + */ +export const getProfile = async (refid: string, name?: string) => { + const profile = await utils.readProfile(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + // Get Score + let hiscore_array = Array(Math.floor((((GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8)).fill(0); + let clear_medal = Array(GAME_MAX_MUSIC_ID).fill(0); + let clear_medal_sub = Array(GAME_MAX_MUSIC_ID).fill(0); + + const scoresData = await utils.readScores(refid, version); + for (const key in scoresData.scores) { + const keyData = key.split(':'); + const score = scoresData.scores[key]; + const music = parseInt(keyData[0], 10); + const sheet = parseInt(keyData[1], 10); + + if (music > GAME_MAX_MUSIC_ID) { + continue; + } + if ([0, 1, 2, 3].indexOf(sheet) == -1) { + continue; + } + + const medal = { + 100: 1, + 200: 2, + 300: 3, + 400: 5, + 500: 5, + 600: 6, + 700: 7, + 800: 9, + 900: 10, + 1000: 11, + 1100: 15, + }[score.clear_type]; + clear_medal[music] = clear_medal[music] | (medal << (sheet * 4)); + + const hiscore_index = (music * 4) + sheet; + const hiscore_byte_pos = Math.floor((hiscore_index * 17) / 8); + const hiscore_bit_pos = ((hiscore_index * 17) % 8); + const hiscore_value = score.score << hiscore_bit_pos; + hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (hiscore_value & 0xFF); + hiscore_array[hiscore_byte_pos + 1] = hiscore_array[hiscore_byte_pos + 1] | ((hiscore_value >> 8) & 0xFF); + hiscore_array[hiscore_byte_pos + 2] = hiscore_array[hiscore_byte_pos + 2] | ((hiscore_value >> 16) & 0xFF); + } + + let player: any = { + base: { + name: K.ITEM('str', profile.name), + g_pm_id: K.ITEM('str', '1234-5678'), + staff: K.ITEM('s8', 0), + is_conv: K.ITEM('s8', -1), + + // TODO: replace with real data + total_play_cnt: K.ITEM('s32', 100), + today_play_cnt: K.ITEM('s16', 50), + consecutive_days: K.ITEM('s16', 365), + my_best: K.ARRAY('s16', Array(20).fill(-1)), + latest_music: K.ARRAY('s16', [-1, -1, -1]), + active_fr_num: K.ITEM('u8', 0), + clear_medal: K.ARRAY('u16', clear_medal), + clear_medal_sub: K.ARRAY('u8', clear_medal_sub), + }, + player_card: { + // TODO: replace with real data + best_music: K.ARRAY('s16', [-1, -1, -1]), + }, + netvs: { + get_ojama: K.ARRAY('s32', [0, 0]), + rank_point: K.ITEM('s32', 0), + play_point: K.ITEM('s32', 0), + record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), + rank: K.ITEM('u8', 0), + dialog: [ + K.ITEM('str', 'dialog#0'), + K.ITEM('str', 'dialog#1'), + K.ITEM('str', 'dialog#2'), + K.ITEM('str', 'dialog#3'), + K.ITEM('str', 'dialog#4'), + K.ITEM('str', 'dialog#5'), + ], + ojama_condition: K.ARRAY('s8', Array(74).fill(0)), + set_ojama: K.ARRAY('s8', [0, 0, 0]), + set_recommend: K.ARRAY('s8', [0, 0, 0]), + jewelry: K.ARRAY('s8', Array(15).fill(0)), + }, + hiscore: K.ITEM('bin', Buffer.from(hiscore_array)) + }; + + // Add version specific datas + const params = await utils.readParams(refid, version); + utils.addExtraData(player, params, extraData); + + return player; +} + +/** + * Unformat and write the end game data into DB + */ +export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + if (!refid) return send.deny(); + + const params = await utils.readParams(refid, version); + utils.getExtraData(data, params, extraData); + await utils.writeParams(refid, version, params); + + const scoresData = await utils.readScores(refid, version, true); + + for (const scoreData of $(data).elements('stage')) { + const music = scoreData.number('no', -1); + const sheet = { + 2: 0, + 0: 1, + 1: 2, + 3: 3, + }[scoreData.number('sheet')]; + const clear_type = { + 1: 100, + 2: 200, + 3: 300, + 5: 500, + 6: 600, + 7: 700, + 9: 800, + 10: 900, + 11: 1000, + 15: 1100, + }[(scoreData.number('n_data') >> (sheet * 4)) & 0x000F]; + const score = scoreData.number('score', 0); + + const key = `${music}:${sheet}`; + + if (!scoresData.scores[key]) { + scoresData.scores[key] = { + score, + cnt: 1, + clear_type + }; + } else { + scoresData.scores[key] = { + score: Math.max(score, scoresData.scores[key].score), + cnt: scoresData.scores[key].cnt + 1, + clear_type: Math.max(clear_type, scoresData.scores[key].clear_type || 0) + }; + } + } + + utils.writeScores(refid, version, scoresData); + + const profile = await utils.readProfile(refid); + + const result = { + pref: K.ITEM('s8', -1), + name: K.ITEM('str', profile.name) + } + + send.object(result); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const version: string = 'v20'; +const GAME_MAX_MUSIC_ID = 1150; + +const extraData: ExtraData = { + mode: { type: 'u8', path: 'base', pathSrc: '', default: 0 }, + button: { type: 's8', path: 'base', pathSrc: '', default: 0 }, + last_play_flag: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + medal_and_friend: { type: 'u8', path: 'base', pathSrc: '', default: 0 }, + category: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + sub_category: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + chara: { type: 's16', path: 'base', pathSrc: '', default: -1 }, + chara_category: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + collabo: { type: 'u8', path: 'base', pathSrc: '', default: 255 }, + sheet: { type: 'u8', path: 'base', pathSrc: '', default: 0 }, + tutorial: { type: 's8', path: 'base', pathSrc: '', default: 0 }, + music_open_pt: { type: 's32', path: 'base', pathSrc: '', default: 0 }, + option: { type: 's32', path: 'base', pathSrc: '', default: 0 }, + music: { type: 's16', path: 'base', pathSrc: '', default: -1 }, + ep: { type: 'u16', path: 'base', pathSrc: '', default: 0 }, + sp_color_flg: { type: 's32', path: 'base', pathSrc: '', default: [0, 0], isArray: true }, + read_news: { type: 's32', path: 'base', pathSrc: '', default: 0 }, + consecutive_days_coupon: { type: 's16', path: 'base', pathSrc: '', default: 0 }, + + title: { type: 'u8', path: 'player_card', pathSrc: '', default: [0, 1], isArray: true }, + frame: { type: 'u8', path: 'player_card', pathSrc: '', default: 0 }, + base: { type: 'u8', path: 'player_card', pathSrc: '', default: 0 }, + seal: { type: 'u8', path: 'player_card', pathSrc: '', default: [0, 0], isArray: true }, + get_title: { type: 's32', path: 'player_card', pathSrc: '', default: [0, 0, 0, 0], isArray: true }, + get_frame: { type: 's32', path: 'player_card', pathSrc: '', default: 0 }, + get_base: { type: 's32', path: 'player_card', pathSrc: '', default: 0 }, + get_seal: { type: 's32', path: 'player_card', pathSrc: '', default: [0, 0], isArray: true }, + + get_title_ex: { type: 's32', path: 'player_card_ex', default: 0 }, + get_frame_ex: { type: 's32', path: 'player_card_ex', default: 0 }, + get_base_ex: { type: 's32', path: 'player_card_ex', default: 0 }, + get_seal_ex: { type: 's32', path: 'player_card_ex', default: 0 }, + + sp: { type: 's32', path: 'sp_data', default: 0 }, + + reflec: { type: 's8', path: 'reflec_data', default: [0, 0], isArray: true }, + + genre: { type: 's8', path: 'navigate', default: 0 }, + image: { type: 's8', path: 'navigate', default: 0 }, + level: { type: 's8', path: 'navigate', default: 0 }, + ojama: { type: 's8', path: 'navigate', default: 0 }, + limit_num: { type: 's16', path: 'navigate', default: 0 }, + button__1: { type: 's8', path: 'navigate', default: 0 }, + life: { type: 's8', path: 'navigate', default: 0 }, + progress: { type: 's16', path: 'navigate', default: 0 }, +} \ No newline at end of file diff --git a/popn@asphyxia/handler/lapistoria.ts b/popn@asphyxia/handler/lapistoria.ts new file mode 100644 index 0000000..925e9f2 --- /dev/null +++ b/popn@asphyxia/handler/lapistoria.ts @@ -0,0 +1,441 @@ +import { AchievementsLapistoria } from "../models/achievements"; +import { ExtraData, Phase, Profile } from "../models/common"; +import * as utils from "./utils"; + +export const setRoutes = () => { + R.Route(`info22.common`, getInfo); + R.Route(`player22.new`, newPlayer); + R.Route(`player22.read`, read); + R.Route(`player22.write_music`, writeScore); + R.Route(`player22.write`, write); +} + +/** + * Return info22.common informations (phase, etc...) + */ +const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const result: any = { + phase: [], + story: [], + }; + + for (const elt of phase) { + result.phase.push({ + event_id: K.ITEM('s16', elt.id), + phase: K.ITEM('s16', elt.p), + }); + } + + for (let i = 0; i < 173; ++i) { + result.story.push({ + story_id: K.ITEM('u32', i), + is_limited: K.ITEM('bool', false), + limit_date: K.ITEM('u64', BigInt(0)), + }); + } + + return send.object(result); +}; + +/** + * Create a new profile and send it. + */ +const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const name = $(data).str('name'); + + send.object(await getProfile(refid, name)); +}; + +/** + * Read a profile and send it. + */ +const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + send.object(await getProfile(refid)); +}; + +const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const music = $(data).number('music_num'); + const sheet = $(data).number('sheet_num'); + const clear_type = { + 1: 100, + 2: 200, + 3: 300, + 4: 400, + 5: 500, + 6: 600, + 7: 700, + 8: 800, + 9: 900, + 10: 1000, + 11: 1100, + }[$(data).number('clearmedal')]; + const score = $(data).number('score'); + + const key = `${music}:${sheet}`; + + const scoresData = await utils.readScores(refid, version, true); + if (!scoresData.scores[key]) { + scoresData.scores[key] = { + score, + cnt: 1, + clear_type + }; + } else { + scoresData.scores[key] = { + score: Math.max(score, scoresData.scores[key].score), + cnt: scoresData.scores[key].cnt + 1, + clear_type: Math.max(clear_type, scoresData.scores[key].clear_type || 0) + }; + } + + utils.writeScores(refid, version, scoresData); + + send.success(); +}; + +/** + * Get/create the profile based on refid + * @param refid the profile refid + * @param name if defined, create/update the profile with the given name + * @returns + */ +const getProfile = async (refid: string, name?: string) => { + const profile = await utils.readProfile(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + let player: any = { + result: K.ITEM('s8', 0), + account: { + name: K.ITEM('str', profile.name), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + staff: K.ITEM('s8', 0), + is_conv: K.ITEM('s8', 0), + item_type: K.ITEM('s16', 0), + item_id: K.ITEM('s16', 0), + license_data: K.ARRAY('s16', [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]), + + // TODO: replace with real data + total_play_cnt: K.ITEM('s16', 100), + today_play_cnt: K.ITEM('s16', 50), + consecutive_days: K.ITEM('s16', 365), + total_days: K.ITEM('s16', 366), + interval_day: K.ITEM('s16', 1), + my_best: K.ARRAY('s16', Array(10).fill(-1)), + latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), + active_fr_num: K.ITEM('u8', 0), + }, + netvs: { + rank_point: K.ITEM('s32', 0), + record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), + rank: K.ITEM('u8', 0), + vs_rank_old: K.ITEM('u8', 0), + dialog: [ + K.ITEM('str', 'dialog#0'), + K.ITEM('str', 'dialog#1'), + K.ITEM('str', 'dialog#2'), + K.ITEM('str', 'dialog#3'), + K.ITEM('str', 'dialog#4'), + K.ITEM('str', 'dialog#5'), + ], + ojama_condition: K.ARRAY('s8', Array(74).fill(0)), + set_ojama: K.ARRAY('s8', [0, 0, 0]), + set_recommend: K.ARRAY('s8', [0, 0, 0]), + netvs_play_cnt: K.ITEM('u32', 0), + }, + custom_cate: { + valid: K.ITEM('s8', 0), + lv_min: K.ITEM('s8', -1), + lv_max: K.ITEM('s8', -1), + medal_min: K.ITEM('s8', -1), + medal_max: K.ITEM('s8', -1), + friend_no: K.ITEM('s8', -1), + score_flg: K.ITEM('s8', -1), + }, + + music: [], + item: [], + achievement: [], + chara_param: [], + story: [], + }; + + // Add Score + const scoresData = await utils.readScores(refid, version); + for (const key in scoresData.scores) { + const keyData = key.split(':'); + const score = scoresData.scores[key]; + const music = parseInt(keyData[0], 10); + const sheet = parseInt(keyData[1], 10); + + if (music > GAME_MAX_MUSIC_ID) { + continue; + } + if ([0, 1, 2, 3].indexOf(sheet) == -1) { + continue; + } + + player.music.push({ + music_num: K.ITEM('s16', music), + sheet_num: K.ITEM('u8', sheet), + cnt: K.ITEM('s16', score.cnt), + score: K.ITEM('s32', score.score), + clear_type: K.ITEM('u8', { + 100: 1, + 200: 2, + 300: 3, + 400: 4, + 500: 5, + 600: 6, + 700: 7, + 800: 8, + 900: 9, + 1000: 10, + 1100: 11, + }[score.clear_type]), + old_score: K.ITEM('s32', 0), + old_clear_type: K.ITEM('u8', 0), + }); + } + + // Add achievements + const achievements = await utils.readAchievements(refid, version, defaultAchievements); + + const profileAchievements = achievements.achievements || { '0': 0 }; + for (const achievement in profileAchievements) { + player.achievement.push({ + type: K.ITEM('u8', parseInt(achievement, 10)), + count: K.ITEM('u32', profileAchievements[achievement]), + }); + } + + const profileCharas = achievements.charas || {}; + for (const chara_id in profileCharas) { + player.chara_param.push({ + chara_id: K.ITEM('u16', parseInt(chara_id, 10)), + friendship: K.ITEM('u16', profileCharas[chara_id]), + }); + } + + const profileStories = achievements.stories || {}; + for (const story_id in profileStories) { + const story = profileStories[story_id]; + player.story.push({ + story_id: K.ITEM('u32', parseInt(story_id, 10)), + chapter_id: K.ITEM('u32', story.chapter_id), + gauge_point: K.ITEM('u16', story.gauge_point), + is_cleared: K.ITEM('bool', story.is_cleared), + clear_chapter: K.ITEM('u32', story.clear_chapter), + }); + } + + const profileItems = achievements.items || {}; + for (const key in profileItems) { + const keyData = key.split(':'); + const type = parseInt(keyData[0], 10); + const id = parseInt(keyData[1], 10); + + const item: any = { + type: K.ITEM('u8', type), + id: K.ITEM('u16', id), + param: K.ITEM('u16', profileItems[key]), + is_new: K.ITEM('bool', 0), + }; + + player.item.push(item); + } + + // Add version specific datas + const params = await utils.readParams(refid, version); + utils.addExtraData(player, params, extraData); + + return player; +} + +const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const params = await utils.readParams(refid, version); + const achievements = await utils.readAchievements(refid, version, defaultAchievements); + + utils.getExtraData(data, params, extraData); + + // achievements + let achievement = _.get(data, 'achievement', []); + if (!achievements.achievements) { + achievements.achievements = { '0': 0 }; + } + + if (!_.isArray(achievement)) { + achievement = [achievement]; + } + + for (const elt of achievement) { + const id = $(elt).number('type'); + const cnt = $(elt).number('count'); + + achievements.achievements[id] = cnt; + } + + // medals + let stories = _.get(data, 'story', []); + if (!achievements.stories) { + achievements.stories = {}; + } + + if (!_.isArray(stories)) { + stories = [stories]; + } + + for (const story of stories) { + const id = $(story).number('story_id'); + const chapter_id = $(story).number('chapter_id'); + const gauge_point = $(story).number('gauge_point'); + const is_cleared = $(story).bool('is_cleared'); + const clear_chapter = $(story).number('clear_chapter'); + + achievements.stories[id] = { + chapter_id, + gauge_point, + is_cleared, + clear_chapter, + }; + } + + // items + let items = _.get(data, 'item', []); + if (!achievements.items) { + achievements.items = {}; + } + + if (!_.isArray(items)) { + items = [items]; + } + + for (const item of items) { + const type = $(item).number('type'); + const id = $(item).number('id'); + const param = $(item).number('param'); + + const key = `${type}:${id}`; + + achievements.items[key] = param; + } + + // charas + let charas = _.get(data, 'chara_param', []); + if (!achievements.charas) { + achievements.charas = {}; + } + + if (!_.isArray(charas)) { + charas = [charas]; + } + + for (const chara of charas) { + const id = $(chara).number('chara_id'); + const param = $(chara).number('friendship'); + + achievements.charas[id] = param; + } + + await utils.writeParams(refid, version, params); + await utils.writeAchievements(refid, version, achievements); + + send.success(); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const version: string = 'v22'; +const GAME_MAX_MUSIC_ID = 1422; + +const defaultAchievements: AchievementsLapistoria = { + collection: 'achievements', + version: 'v22', + achievements: {}, + stories: {}, + items: {}, + charas: {}, +} + +const phase: Phase[] = [ + { id: 0, p: 16 }, + { id: 1, p: 11 }, + { id: 2, p: 11 }, + { id: 3, p: 24 }, + { id: 4, p: 2 }, + { id: 5, p: 2 }, + { id: 6, p: 1 }, + { id: 7, p: 1 }, + { id: 8, p: 2 }, + { id: 9, p: 11 }, + { id: 10, p: 2 }, + { id: 11, p: 3 }, + { id: 12, p: 1 }, + { id: 13, p: 2 }, + { id: 14, p: 4 }, + { id: 15, p: 2 }, + { id: 16, p: 2 }, + { id: 17, p: 12 }, + { id: 18, p: 2 }, + { id: 19, p: 7 }, +]; + +const extraData: ExtraData = { + tutorial: { type: 's8', path: 'account', default: 0 }, + read_news: { type: 's16', path: 'account', default: 0 }, + + hispeed: { type: 's16', path: 'option', default: 0 }, + popkun: { type: 'u8', path: 'option', default: 0 }, + hidden: { type: 'bool', path: 'option', default: 0 }, + hidden_rate: { type: 's16', path: 'option', default: 0 }, + sudden: { type: 'bool', path: 'option', default: 0 }, + sudden_rate: { type: 's16', path: 'option', default: 0 }, + randmir: { type: 's8', path: 'option', default: 0 }, + gauge_type: { type: 's8', path: 'option', default: 0 }, + ojama_0: { type: 'u8', path: 'option', default: 0 }, + ojama_1: { type: 'u8', path: 'option', default: 0 }, + forever_0: { type: 'bool', path: 'option', default: 0 }, + forever_1: { type: 'bool', path: 'option', default: 0 }, + full_setting: { type: 'bool', path: 'option', default: 0 }, + + ep: { type: 'u16', path: 'info', default: 0 }, + ap: { type: 'u16', path: 'info', default: 0 }, + + effect: { type: 'u16', path: 'customize', default: 0 }, + hukidashi: { type: 'u16', path: 'customize', default: 0 }, + font: { type: 'u16', path: 'customize', default: 0 }, + comment_1: { type: 'u16', path: 'customize', default: 0 }, + comment_2: { type: 'u16', path: 'customize', default: 0 }, + + mode: { type: 'u8', path: 'config', default: 0 }, + chara: { type: 's16', path: 'config', default: -1 }, + music: { type: 's16', path: 'config', default: -1 }, + sheet: { type: 'u8', path: 'config', default: 0 }, + category: { type: 's8', path: 'config', default: 1 }, + sub_category: { type: 's8', path: 'config', default: -1 }, + chara_category: { type: 's8', path: 'config', default: -1 }, + story_id: { type: 's16', path: 'config', default: -1 }, + course_id: { type: 's16', path: 'config', default: -1 }, + course_folder: { type: 's8', path: 'config', default: -1 }, + story_folder: { type: 's8', path: 'config', default: -1 }, + ms_banner_disp: { type: 's8', path: 'config', default: 0 }, + ms_down_info: { type: 's8', path: 'config', default: 0 }, + ms_side_info: { type: 's8', path: 'config', default: 0 }, + ms_raise_type: { type: 's8', path: 'config', default: 0 }, + ms_rnd_type: { type: 's8', path: 'config', default: 0 }, +} \ No newline at end of file diff --git a/popn@asphyxia/handler/player.ts b/popn@asphyxia/handler/player.ts deleted file mode 100644 index a9949fb..0000000 --- a/popn@asphyxia/handler/player.ts +++ /dev/null @@ -1,519 +0,0 @@ -import { Profile } from '../models/profile'; -import { Scores } from '../models/scores'; -import { getInfo, M39_EXTRA_DATA } from './common'; -import { getVersion } from './utils'; - -const getPlayer = async (refid: string, version: string, name?: string) => { - const profile = await readProfile(refid); - - if (name && name.length > 0) { - profile.name = name; - await writeProfile(refid, profile); - } - - let player: any = { - result: K.ITEM('s8', 0), - account: { - name: K.ITEM('str', profile.name), - g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), - - // Fixed stats - total_play_cnt: K.ITEM('s16', 100), - today_play_cnt: K.ITEM('s16', 50), - consecutive_days: K.ITEM('s16', 365), - total_days: K.ITEM('s16', 366), - interval_day: K.ITEM('s16', 1), - - // TODO: do these - my_best: K.ARRAY('s16', Array(10).fill(-1)), - latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), - }, - - netvs: { - record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), - dialog: [ - K.ITEM('str', 'dialog#0'), - K.ITEM('str', 'dialog#1'), - K.ITEM('str', 'dialog#2'), - K.ITEM('str', 'dialog#3'), - K.ITEM('str', 'dialog#4'), - K.ITEM('str', 'dialog#5'), - ], - ojama_condition: K.ARRAY('s8', Array(74).fill(0)), - set_ojama: K.ARRAY('s8', [0, 0, 0]), - set_recommend: K.ARRAY('s8', [0, 0, 0]), - netvs_play_cnt: K.ITEM('u32', 0), - }, - - eaappli: { - relation: K.ITEM('s8', 0), - }, - - stamp: [], - item: [], - chara_param: [], - medal: [], - }; - - const profileStamps = profile.stamps[version] || { '0': 0 }; - - for (const stamp in profileStamps) { - player.stamp.push({ - stamp_id: K.ITEM('s16', parseInt(stamp, 10)), - cnt: K.ITEM('s16', profileStamps[stamp]), - }); - } - - const profileCharas = profile.charas[version] || {}; - - for (const chara_id in profileCharas) { - player.chara_param.push({ - chara_id: K.ITEM('u16', parseInt(chara_id, 10)), - friendship: K.ITEM('u16', profileCharas[chara_id]), - }); - } - - const profileMedals = profile.medals[version] || {}; - - for (const medal_id in profileMedals) { - const medal = profileMedals[medal_id]; - player.medal.push({ - medal_id: K.ITEM('s16', parseInt(medal_id, 10)), - level: K.ITEM('s16', medal.level), - exp: K.ITEM('s32', medal.exp), - set_count: K.ITEM('s32', medal.set_count), - get_count: K.ITEM('s32', medal.get_count), - }); - } - - const profileItems = profile.items[version] || {}; - - for (const key in profileItems) { - const keyData = key.split(':'); - const type = parseInt(keyData[0], 10); - const id = parseInt(keyData[1], 10); - - const item: any = { - type: K.ITEM('u8', type), - id: K.ITEM('u16', id), - param: K.ITEM('u16', profileItems[key]), - is_new: K.ITEM('bool', 0), - }; - - if (version != 'v23') { - item.get_time = K.ITEM('u64', BigInt(0)); - } - - player.item.push(item); - } - - // EXTRA DATA - if (M39_EXTRA_DATA[version]) { - for (const field in M39_EXTRA_DATA[version]) { - const fieldMetaData = M39_EXTRA_DATA[version][field]; - if (fieldMetaData.isArray) { - _.set( - player, - `${fieldMetaData.path}.${field}`, - K.ARRAY( - fieldMetaData.type as any, - _.get(profile, `extras.${version}.${field}`, fieldMetaData.default) - ) - ); - } else { - _.set( - player, - `${fieldMetaData.path}.${field}`, - K.ITEM( - fieldMetaData.type as any, - _.get(profile, `extras.${version}.${field}`, fieldMetaData.default) - ) - ); - } - } - } - - // Extra Fixed Data - if (version == 'v24') { - player = { - ...player, - navi_data: { - raisePoint: K.ARRAY('s32', [-1, -1, -1, -1, -1]), - navi_param: { - navi_id: K.ITEM('u16', 0), - friendship: K.ITEM('s32', 0), - }, - }, - - area: { - area_id: K.ITEM('u32', 0), - chapter_index: K.ITEM('u8', 0), - gauge_point: K.ITEM('u16', 0), - is_cleared: K.ITEM('bool', 0), - diary: K.ITEM('u32', 0), - }, - - mission: [ - { - mission_id: K.ITEM('u32', 170), - gauge_point: K.ITEM('u32', 0), - mission_comp: K.ITEM('u32', 0), - }, - { - mission_id: K.ITEM('u32', 157), - gauge_point: K.ITEM('u32', 0), - mission_comp: K.ITEM('u32', 0), - }, - { - mission_id: K.ITEM('u32', 47), - gauge_point: K.ITEM('u32', 0), - mission_comp: K.ITEM('u32', 0), - }, - ], - }; - } - - return player; -}; - -export const newPlayer: EPR = async (req, data, send) => { - const refid = $(data).str('ref_id'); - if (!refid) return send.deny(); - - const version = getVersion(req); - const name = $(data).str('name'); - - send.object(await getPlayer(refid, version, name)); -}; - -export const read: EPR = async (req, data, send) => { - const refid = $(data).str('ref_id'); - if (!refid) return send.deny(); - - const version = getVersion(req); - - send.object(await getPlayer(refid, version)); -}; - -export const readScore: EPR = async (req, data, send) => { - const refid = $(data).str('ref_id'); - if (!refid) return send.deny(); - - const scoresData = await readScores(refid); - const version = getVersion(req); - const result: any = { - music: [], - }; - - if(version == 'v24') { - for (const key in scoresData.scores) { - const keyData = key.split(':'); - const score = scoresData.scores[key]; - const music = parseInt(keyData[0], 10); - const sheet = parseInt(keyData[1], 10); - result.music.push({ - music_num: K.ITEM('s16', music), - sheet_num: K.ITEM('u8', sheet), - score: K.ITEM('s32', score.score), - clear_type: K.ITEM('u8', score.clear_type || 0), - clear_rank: K.ITEM('u8', score.clear_rank || 0), - cnt: K.ITEM('s16', score.cnt), - }); - } - } else if(version == 'v23') { - for (const key in scoresData.scores) { - const keyData = key.split(':'); - const score = scoresData.scores[key]; - const music = parseInt(keyData[0], 10); - const sheet = parseInt(keyData[1], 10); - result.music.push({ - music_num: K.ITEM('s16', music), - sheet_num: K.ITEM('u8', sheet), - score: K.ITEM('s32', score.score), - clear_type: K.ITEM('u8', score.clearmedal || 0), - cnt: K.ITEM('s16', score.cnt), - old_score: K.ITEM('s32', 0), - old_clear_type: K.ITEM('u8', 0), - }); - } - } - - send.object(result); -}; - -export const writeMusic: EPR = async (req, data, send) => { - const refid = $(data).str('ref_id'); - if (!refid) return send.deny(); - - const music = $(data).number('music_num', -1); - const sheet = $(data).number('sheet_num', -1); - const clear_type = $(data).number('clear_type'); - const clear_rank = $(data).number('clear_rank'); - const clearmedal = $(data).number('clearmedal'); - const score = $(data).number('score', 0); - - if (music < 0 || sheet < 0) { - return send.deny(); - } - - const key = `${music}:${sheet}`; - - const scoresData = await readScores(refid); - if (!scoresData.scores[key]) { - scoresData.scores[key] = { - score, - cnt: 1, - }; - } else { - scoresData.scores[key] = { - score: Math.max(score, scoresData.scores[key].score), - cnt: scoresData.scores[key].cnt + 1, - }; - } - - if (clear_type) { - scoresData.scores[key].clear_type = Math.max(clear_type, scoresData.scores[key].clear_type || 0); - } - - if (clear_rank) { - scoresData.scores[key].clear_rank = Math.max(clear_rank, scoresData.scores[key].clear_rank || 0); - } - - if (clearmedal) { - scoresData.scores[key].clearmedal = Math.max(clearmedal, scoresData.scores[key].clearmedal || 0); - } - - writeScores(refid, scoresData); - - const version = getVersion(req); - if (version == 'v24') { - - const p = await readProfile(refid); - - const settings = [ - 'hispeed', - 'popkun', - 'hidden', - 'hidden_rate', - 'sudden', - 'sudden_rate', - 'randmir', - 'ojama_0', - 'ojama_1', - 'forever_0', - 'forever_1', - 'full_setting', - 'guide_se', - 'judge', - 'slow', - 'fast', - 'mode', - ]; - - for (const setting of settings) { - _.set( - p, - `extras.v24.${setting}`, - _.get(data, `${setting}.@content.0`, _.get(p, `extras.v24.${setting}`, 0)) - ); - } - - _.set(p, `extras.v24.tutorial`, 32767); - - const chara = $(data).number('chara_num'); - if (chara) { - _.set(p, 'extras.v24.chara', chara); - } - - const music = $(data).number('music_num'); - if (music) { - _.set(p, 'extras.v24.music', music); - } - - const sheet = $(data).number('sheet_num'); - if (sheet) { - _.set(p, 'extras.v24.sheet', sheet); - } - - writeProfile(refid, p); - } - - send.success(); -}; - -export const write: EPR = async (req, data, send) => { - const refid = $(data).str('ref_id'); - if (!refid) return send.deny(); - - const version = getVersion(req); - const profile = await readProfile(refid); - - const writeData: Partial = {}; - - if (M39_EXTRA_DATA[version]) { - const extraFields = M39_EXTRA_DATA[version]; - for (const field in extraFields) { - const fieldMetaData = extraFields[field]; - let value = _.get(data, `${fieldMetaData.path}.${field}.@content`); - if ( value == 'undefined' && value == null ) { - continue; - } - - if (_.isArray(value) && value.length == 1) { - value = value[0]; - } - - _.set(writeData, `extras.${version}.${field}`, value); - } - } - - const newProfile:Profile = { - ...profile, - ...writeData, - }; - - // stamps - let stamps = _.get(data, 'stamp', []); - if (!newProfile.stamps[version]) { - newProfile.stamps[version] = { '0': 0 }; - } - - if (!_.isArray(stamps)) { - stamps = [stamps]; - } - - for (const stamp of stamps) { - const id = $(stamp).number('stamp_id'); - const cnt = $(stamp).number('cnt'); - - newProfile.stamps[version][id] = cnt; - } - - // medals - let medals = _.get(data, 'medal', []); - if (!newProfile.medals[version]) { - newProfile.medals[version] = {}; - } - - if (!_.isArray(medals)) { - medals = [medals]; - } - - for (const medal of medals) { - const id = $(medal).number('medal_id'); - const level = $(medal).number('level'); - const exp = $(medal).number('exp'); - const set_count = $(medal).number('set_count'); - const get_count = $(medal).number('get_count'); - - newProfile.medals[version][id] = { - level, - exp, - set_count, - get_count, - }; - } - - // items - let items = _.get(data, 'item', []); - if (!newProfile.items[version]) { - newProfile.items[version] = {}; - } - - if (!_.isArray(items)) { - items = [items]; - } - - for (const item of items) { - const type = $(item).number('type'); - const id = $(item).number('id'); - const param = $(item).number('param'); - - const key = `${type}:${id}`; - - newProfile.items[version][key] = param; - } - - // charas - let charas = _.get(data, 'chara_param', []); - if (!newProfile.charas[version]) { - newProfile.charas[version] = {}; - } - - if (!_.isArray(charas)) { - charas = [charas]; - } - - for (const chara of charas) { - const id = $(chara).number('chara_id'); - const param = $(chara).number('friendship'); - - newProfile.charas[version][id] = param; - } - - await writeProfile(refid, newProfile); - send.success(); -}; - -export const start: EPR = async (req, data, send) => { - const result = { - play_id: K.ITEM('s32', 1), - ...getInfo(req), - }; - await send.object(result); -}; - -export const buy: EPR = async (req, data, send) => { - const refid = $(data).str('ref_id'); - if (!refid) return send.deny(); - - const type = $(data).number('type', -1); - const id = $(data).number('id', -1); - const param = $(data).number('param', 0); - const version = getVersion(req); - - if (type < 0 || id < 0) { - return send.deny(); - } - - const key = `${type}:${id}`; - - const profile = await readProfile(refid); - if (!profile.items[version]) { - profile.items[version] = {}; - } - profile.items[version][key] = param; - await writeProfile(refid, profile); - send.success(); -}; - -const defaultProfile:Profile = { - collection: 'profile', - - name: 'ゲスト', - - stamps: {}, - medals: {}, - items: {}, - charas: {}, - - extras: {}, -}; - -async function readProfile(refid: string): Promise { - const profile = await DB.FindOne(refid, { collection: 'profile'} ) - return profile || defaultProfile -} - -async function writeProfile(refid: string, profile: Profile) { - await DB.Upsert(refid, { collection: 'profile'}, profile) -} - -async function readScores(refid: string): Promise { - const score = await DB.FindOne(refid, { collection: 'scores'} ) - return score || { collection: 'scores', scores: {}} -} - -async function writeScores(refid: string, scores: Scores) { - await DB.Upsert(refid, { collection: 'scores'}, scores) -} \ No newline at end of file diff --git a/popn@asphyxia/handler/sunny.ts b/popn@asphyxia/handler/sunny.ts new file mode 100644 index 0000000..903290a --- /dev/null +++ b/popn@asphyxia/handler/sunny.ts @@ -0,0 +1,321 @@ +import { ExtraData } from "../models/common"; +import * as utils from "./utils"; + +/** + * Return the current phases of the game. + */ +export const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const result = { + ir_phase: K.ITEM('s32', 0), + music_open_phase: K.ITEM('s32', 8), + collabo_phase: K.ITEM('s32', 8), + personal_event_phase: K.ITEM('s32', 10), + shop_event_phase: K.ITEM('s32', 6), + netvs_phase: K.ITEM('s32', 0), + card_phase: K.ITEM('s32', 9), + other_phase: K.ITEM('s32', 9), + local_matching_enable: K.ITEM('s32', 1), + n_matching_sec: K.ITEM('s32', 60), + l_matching_sec: K.ITEM('s32', 60), + is_check_cpu: K.ITEM('s32', 0), + week_no: K.ITEM('s32', 0), + sel_ranking: K.ARRAY('s16', [-1, -1, -1, -1, -1]), + up_ranking: K.ARRAY('s16', [-1, -1, -1, -1, -1]), + }; + + return send.object(result); +}; + +/** + * Create a new profile and send it. + */ +export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const name = $(data).str('name'); + + send.object(await getProfile(refid, name)); +}; + +/** + * Read a profile and send it. + */ +export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + send.object(await getProfile(refid)); +}; + +/** + * Get/create the profile based on refid and format it + * @param refid the profile refid + * @param name if defined, create/update the profile with the given name + */ +export const getProfile = async (refid: string, name?: string) => { + const profile = await utils.readProfile(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + // Get Score + let hiscore_array = Array(Math.floor((((GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8)).fill(0); + let clear_medal = Array(GAME_MAX_MUSIC_ID).fill(0); + let clear_medal_sub = Array(GAME_MAX_MUSIC_ID).fill(0); + + const scoresData = await utils.readScores(refid, version); + for (const key in scoresData.scores) { + const keyData = key.split(':'); + const score = scoresData.scores[key]; + const music = parseInt(keyData[0], 10); + const sheet = parseInt(keyData[1], 10); + + if (music > GAME_MAX_MUSIC_ID) { + continue; + } + if ([0, 1, 2, 3].indexOf(sheet) == -1) { + continue; + } + + const medal = { + 100: 1, + 200: 2, + 300: 3, + 400: 5, + 500: 5, + 600: 6, + 700: 7, + 800: 9, + 900: 10, + 1000: 11, + 1100: 15, + }[score.clear_type]; + clear_medal[music] = clear_medal[music] | (medal << (sheet * 4)); + + const hiscore_index = (music * 4) + sheet; + const hiscore_byte_pos = Math.floor((hiscore_index * 17) / 8); + const hiscore_bit_pos = ((hiscore_index * 17) % 8); + const hiscore_value = score.score << hiscore_bit_pos; + hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (hiscore_value & 0xFF); + hiscore_array[hiscore_byte_pos + 1] = hiscore_array[hiscore_byte_pos + 1] | ((hiscore_value >> 8) & 0xFF); + hiscore_array[hiscore_byte_pos + 2] = hiscore_array[hiscore_byte_pos + 2] | ((hiscore_value >> 16) & 0xFF); + } + + let player: any = { + base: { + name: K.ITEM('str', profile.name), + g_pm_id: K.ITEM('str', '1234-5678'), + staff: K.ITEM('s8', 0), + is_conv: K.ITEM('s8', -1), + collabo: K.ITEM('u8', 255), + + // TODO: replace with real data + total_play_cnt: K.ITEM('s32', 100), + today_play_cnt: K.ITEM('s16', 50), + consecutive_days: K.ITEM('s16', 365), + my_best: K.ARRAY('s16', Array(20).fill(-1)), + latest_music: K.ARRAY('s16', [-1, -1, -1]), + active_fr_num: K.ITEM('u8', 0), + clear_medal: K.ARRAY('u16', clear_medal), + clear_medal_sub: K.ARRAY('u8', clear_medal_sub), + }, + netvs: { + rank_point: K.ITEM('s32', 0), + record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), + rank: K.ITEM('u8', 0), + vs_rank_old: K.ITEM('s8', 0), + dialog: [ + K.ITEM('str', 'dialog#0'), + K.ITEM('str', 'dialog#1'), + K.ITEM('str', 'dialog#2'), + K.ITEM('str', 'dialog#3'), + K.ITEM('str', 'dialog#4'), + K.ITEM('str', 'dialog#5'), + ], + ojama_condition: K.ARRAY('s8', Array(74).fill(0)), + set_ojama: K.ARRAY('s8', [0, 0, 0]), + set_recommend: K.ARRAY('s8', [0, 0, 0]), + netvs_play_cnt: K.ITEM('u8', 0), + }, + hiscore: K.ITEM('bin', Buffer.from(hiscore_array)), + gakuen_data: { + music_list: K.ITEM('s32', -1), + }, + flying_saucer: { + music_list: K.ITEM('s32', -1), + tune_count: K.ITEM('s32', -1), + clear_norma: K.ITEM('u32', 0), + clear_norma_add: K.ITEM('u32', 0), + }, + triple_journey: { + music_list: K.ITEM('s32', -1), + boss_damage: K.ARRAY('s32', [65534, 65534, 65534, 65534]), + boss_stun: K.ARRAY('s32', [0, 0, 0, 0]), + magic_gauge: K.ITEM('s32', 0), + today_party: K.ITEM('s32', 0), + union_magic: K.ITEM('bool', false), + base_attack_rate: K.ITEM('float', 1.0), + iidx_play_num: K.ITEM('s32', 0), + reflec_play_num: K.ITEM('s32', 0), + voltex_play_num: K.ITEM('s32', 0), + iidx_play_flg: K.ITEM('bool', true), + reflec_play_flg: K.ITEM('bool', true), + voltex_play_flg: K.ITEM('bool', true), + }, + ios: { + continueRightAnswer: K.ITEM('s32', 30), + totalRightAnswer: K.ITEM('s32', 30), + }, + kac2013: { + music_num: K.ITEM('s8', 0), + music: K.ITEM('s16', 0), + sheet: K.ITEM('u8', 0), + }, + baseball_data: { + music_list: K.ITEM('s64', BigInt(-1)), + }, + floor_infection: [ + { + infection_id: K.ITEM('s32', 3), + music_list: K.ITEM('s32', -1), + }, + { + infection_id: K.ITEM('s32', 5), + music_list: K.ITEM('s32', -1), + }, + { + infection_id: K.ITEM('s32', 7), + music_list: K.ITEM('s32', -1), + } + ] + }; + + // Add version specific datas + const params = await utils.readParams(refid, version); + utils.addExtraData(player, params, extraData); + + return player; +} + +/** + * Unformat and write the end game data into DB + */ +export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + if (!refid) return send.deny(); + + const params = await utils.readParams(refid, version); + utils.getExtraData(data, params, extraData); + await utils.writeParams(refid, version, params); + + const scoresData = await utils.readScores(refid, version, true); + + for (const scoreData of $(data).elements('stage')) { + const music = scoreData.number('no', -1); + const sheet = scoreData.number('sheet'); + const clear_type = { + 1: 100, + 2: 200, + 3: 300, + 5: 500, + 6: 600, + 7: 700, + 9: 800, + 10: 900, + 11: 1000, + 15: 1100, + }[(scoreData.number('n_data') >> (sheet * 4)) & 0x000F]; + const score = scoreData.number('score', 0); + + const key = `${music}:${sheet}`; + + if (!scoresData.scores[key]) { + scoresData.scores[key] = { + score, + cnt: 1, + clear_type + }; + } else { + scoresData.scores[key] = { + score: Math.max(score, scoresData.scores[key].score), + cnt: scoresData.scores[key].cnt + 1, + clear_type: Math.max(clear_type, scoresData.scores[key].clear_type || 0) + }; + } + } + + utils.writeScores(refid, version, scoresData); + + const profile = await utils.readProfile(refid); + + const result = { + pref: K.ITEM('s8', -1), + name: K.ITEM('str', profile.name) + } + + send.object(result); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const version: string = 'v21'; +const GAME_MAX_MUSIC_ID = 1350; + +const extraData: ExtraData = { + mode: { type: 'u8', path: 'base', pathSrc: '', default: 0 }, + button: { type: 's8', path: 'base', pathSrc: '', default: 0 }, + last_play_flag: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + medal_and_friend: { type: 'u8', path: 'base', pathSrc: '', default: 0 }, + category: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + sub_category: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + chara: { type: 's16', path: 'base', pathSrc: '', default: -1 }, + chara_category: { type: 's8', path: 'base', pathSrc: '', default: -1 }, + sheet: { type: 'u8', path: 'base', pathSrc: '', default: 0 }, + tutorial: { type: 's8', path: 'base', pathSrc: '', default: 0 }, + music_open_pt: { type: 's8', path: 'base', pathSrc: '', default: 0 }, + option: { type: 's32', path: 'base', pathSrc: '', default: 0 }, + music: { type: 's16', path: 'base', pathSrc: '', default: -1 }, + ep: { type: 'u16', path: 'base', pathSrc: '', default: 0 }, + sp_color_flg: { type: 's32', path: 'base', pathSrc: '', default: [0, 0], isArray: true }, + read_news: { type: 's32', path: 'base', pathSrc: '', default: 0 }, + consecutive_days_coupon: { type: 's16', path: 'base', pathSrc: '', default: 0 }, + gitadora_point: { type: 'u16', path: 'base', pathSrc: '', default: [2000, 2000, 2000], isArray: true }, + gitadora_select: { type: 'u8', path: 'base', pathSrc: '', default: 2 }, + + sp: { type: 's32', path: 'sp_data', default: 0 }, + + point: { type: 'u16', path: 'zoo', default: [0, 0, 0, 0, 0], isArray: true }, + music_list__2: { type: 's32', path: 'zoo', default: [0, 0], isArray: true }, + today_play_flag: { type: 's8', path: 'zoo', default: [0, 0, 0, 0], isArray: true }, + + hair: { type: 'u8', path: 'avatar', pathSrc: '', default: 0 }, + face: { type: 'u8', path: 'avatar', pathSrc: '', default: 0 }, + body: { type: 'u8', path: 'avatar', pathSrc: '', default: 0 }, + effect: { type: 'u8', path: 'avatar', pathSrc: '', default: 0 }, + object: { type: 'u8', path: 'avatar', pathSrc: '', default: 0 }, + comment: { type: 'u8', path: 'avatar', pathSrc: '', default: [0, 0], isArray: true }, + get_hair: { type: 's32', path: 'avatar', pathSrc: '', default: [0, 0], isArray: true }, + get_face: { type: 's32', path: 'avatar', pathSrc: '', default: [0, 0], isArray: true }, + get_body: { type: 's32', path: 'avatar', pathSrc: '', default: [0, 0], isArray: true }, + get_effect: { type: 's32', path: 'avatar', pathSrc: '', default: [0, 0], isArray: true }, + get_object: { type: 's32', path: 'avatar', pathSrc: '', default: [0, 0], isArray: true }, + get_comment_over: { type: 's32', path: 'avatar', pathSrc: '', default: [0, 0, 0], isArray: true }, + get_comment_under: { type: 's32', path: 'avatar', pathSrc: '', default: [0, 0, 0], isArray: true }, + + get_hair__1: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + get_face__1: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + get_body__1: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + get_effect__1: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + get_object__1: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + get_comment_over__1: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + get_comment_under__1: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + new_face: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + new_body: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + new_effect: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + new_object: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + new_comment_over: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, + new_comment_under: { type: 's32', path: 'avatar_add', default: [0, 0], isArray: true }, +} \ No newline at end of file diff --git a/popn@asphyxia/handler/tunestreet.ts b/popn@asphyxia/handler/tunestreet.ts new file mode 100644 index 0000000..5bfdf03 --- /dev/null +++ b/popn@asphyxia/handler/tunestreet.ts @@ -0,0 +1,273 @@ +import * as utils from "./utils"; + +export const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const result = K.ATTR({ game_phase: "2", psp_phase: "2" }); + + return send.object(result); +}; + +/** + * Create a new profile and send it. + */ +export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + if (!refid) return send.deny(); + + const name = $(data).attr()['name']; + + send.object(await getProfile(refid, name)); +}; + +/** + * Read a profile and send it. + */ +export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + if (!refid) return send.deny(); + + send.object(await getProfile(refid)); +}; + +/** + * Get/create the profile based on refid + * @param refid the profile refid + * @param name if defined, create/update the profile with the given name + * @returns + */ +export const getProfile = async (refid: string, name?: string) => { + const profile = await utils.readProfile(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + const params = await utils.readParams(refid, version); + + let binary_profile = Array(2200).fill(0); + let name_binary = profile.name.substr(0, 12); + for (let i = 0; i < name_binary.length; i++) { + binary_profile[i] = name_binary.charAt(i); + } + + binary_profile[13] = { + 0: 0, + 1: 0, + 2: 1, + 3: 1, + 4: 4, + 5: 2, + }[_.get(params, `params.play_mode`, 0)] + + binary_profile[16] = _.get(params, `params.last_play_flag`, 0) & 0xFF + binary_profile[44] = _.get(params, `params.option`, 0) & 0xFF + binary_profile[45] = (_.get(params, `params.option`, 0) >> 8) & 0xFF + binary_profile[46] = (_.get(params, `params.option`, 0) >> 16) & 0xFF + binary_profile[47] = (_.get(params, `params.option`, 0) >> 24) & 0xFF + binary_profile[60] = _.get(params, `params.chara`, 0) & 0xFF + binary_profile[61] = (_.get(params, `params.chara`, 0) >> 8) & 0xFF + binary_profile[62] = _.get(params, `params.music`, 0) & 0xFF + binary_profile[63] = (_.get(params, `params.music`, 0) >> 8) & 0xFF + binary_profile[64] = _.get(params, `params.sheet`, 0) & 0xFF + binary_profile[65] = _.get(params, `params.category`, 0) & 0xFF + binary_profile[67] = _.get(params, `params.medal_and_friend`, 0) & 0xFF + + // Get Score + let hiscore_array = Array(Math.floor((((GAME_MAX_MUSIC_ID * 7) * 17) + 7) / 8)).fill(0); + + const scoresData = await utils.readScores(refid, version); + for (const key in scoresData.scores) { + const keyData = key.split(':'); + const score = scoresData.scores[key]; + const music = parseInt(keyData[0], 10); + const sheet = parseInt(keyData[1], 10); + + if (music > GAME_MAX_MUSIC_ID) { + continue; + } + if (sheet == 0) { + continue; + } + + //flag + const flags = __format_flags_for_score(sheet, score.clear_type); + const flags_index = music * 2 + binary_profile[108 + flags_index] = binary_profile[108 + flags_index] | (flags & 0xFF) + binary_profile[109 + flags_index] = binary_profile[109 + flags_index] | ((flags >> 8) & 0xFF) + + if (sheet == 7 || sheet == 8) { + continue; + } + + const hiscore_index = (music * 7) + { + 9: 0, + 4: 1, + 5: 2, + 6: 3, + 1: 4, + 2: 5, + 3: 6, + }[sheet]; + const hiscore_byte_pos = Math.floor((hiscore_index * 17) / 8); + const hiscore_bit_pos = ((hiscore_index * 17) % 8); + const hiscore_value = score.score << hiscore_bit_pos; + hiscore_array[hiscore_byte_pos] = hiscore_array[hiscore_byte_pos] | (hiscore_value & 0xFF); + hiscore_array[hiscore_byte_pos + 1] = hiscore_array[hiscore_byte_pos + 1] | ((hiscore_value >> 8) & 0xFF); + hiscore_array[hiscore_byte_pos + 2] = hiscore_array[hiscore_byte_pos + 2] | ((hiscore_value >> 16) & 0xFF); + } + + const player = { + b: K.ITEM('bin', Buffer.from(binary_profile)), + hiscore: K.ITEM('bin', Buffer.from(hiscore_array)), + town: K.ITEM('bin', Buffer.alloc(0)), + } + + return player; +} + +const __format_flags_for_score = (sheet: number, clear_type: number) => { + const playedflag = { + 9: 0x2000, + 4: 0x0800, + 5: 0x1000, + 6: 0x4000, + 1: 0x0800, + 2: 0x1000, + 3: 0x4000, + 7: 0, + 8: 0, + }[sheet] + const shift = { + 9: 4, + 4: 0, + 5: 2, + 6: 6, + 1: 0, + 2: 2, + 3: 6, + 7: 9, + 8: 8, + }[sheet] + const flags = { + 100: 0, + 200: 0, + 300: 0, + 400: 1, + 500: 1, + 600: 1, + 700: 1, + 800: 2, + 900: 2, + 1000: 2, + 1100: 3, + }[clear_type] + return (flags << shift) | playedflag +} + +export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + if (!refid) return send.deny(); + + const params = await utils.readParams(refid, version); + + params.params['play_mode'] = parseInt($(data).attr()['play_mode']); + params.params['chara'] = parseInt($(data).attr()['chara_num']); + params.params['option'] = parseInt($(data).attr()['option']); + params.params['last_play_flag'] = parseInt($(data).attr()['last_play_flag']); + params.params['medal_and_friend'] = parseInt($(data).attr()['medal_and_friend']); + params.params['music'] = parseInt($(data).attr()['music_num']); + params.params['sheet'] = parseInt($(data).attr()['sheet_num']); + params.params['category'] = parseInt($(data).attr()['category_num']); + + await utils.writeParams(refid, version, params); + + const scoresData = await utils.readScores(refid, version, true); + + for (const scoreData of $(data).elements('music')) { + const music = parseInt(scoreData.attr()['music_num']); + let sheet = parseInt(scoreData.attr()['sheet_num']); + const score = parseInt(scoreData.attr()['score']); + const data = parseInt(scoreData.attr()['data']); + + if (sheet == 4 || sheet == 5) { + continue; + } + + if(params.params['play_mode'] == 4) { + if ([2, 6, 7].indexOf(sheet) != -1) { + continue; + } + sheet = { + 0: 1, + 1: 2, + 3: 3, + }[sheet] + } else { + sheet = { + 0: 4, + 1: 5, + 2: 9, + 3: 6, + 6: 7, + 7: 8, + }[sheet] + } + + const shift = { + 9: 4, + 4: 0, + 5: 2, + 6: 6, + 1: 0, + 2: 2, + 3: 6, + 7: 9, + 8: 8, + }[sheet] + + let mask = 0x3; + if (sheet == 7 || sheet == 8) { + mask = 0x1; + } + + const flags = (data >> shift) & mask; + const clear_type = { + 0: 100, + 1: 500, + 2: 800, + 3: 1100, + }[flags] + + const key = `${music}:${sheet}`; + + if (!scoresData.scores[key]) { + scoresData.scores[key] = { + score, + cnt: 1, + clear_type + }; + } else { + scoresData.scores[key] = { + score: Math.max(score, scoresData.scores[key].score), + cnt: scoresData.scores[key].cnt + 1, + clear_type: Math.max(clear_type, scoresData.scores[key].clear_type || 0) + }; + } + } + + utils.writeScores(refid, version, scoresData); + + const profile = await utils.readProfile(refid); + + const result = { + pref: K.ITEM('s8', -1), + name: K.ITEM('str', profile.name) + } + + send.object(result); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const version: string = 'v19'; +const GAME_MAX_MUSIC_ID = 1045; \ No newline at end of file diff --git a/popn@asphyxia/handler/usaneko.ts b/popn@asphyxia/handler/usaneko.ts new file mode 100644 index 0000000..09e3994 --- /dev/null +++ b/popn@asphyxia/handler/usaneko.ts @@ -0,0 +1,719 @@ +import { AchievementsUsaneko } from "../models/achievements"; +import { ExtraData, Params, Phase } from "../models/common"; +import * as utils from "./utils"; + +export const setRoutes = () => { + R.Route(`info24.common`, getInfo); + R.Route(`player24.new`, newPlayer); + R.Route(`player24.read`, read); + R.Route(`player24.start`, start); + R.Route(`player24.buy`, buy); + R.Route(`player24.read_score`, readScore); + R.Route(`player24.write_music`, writeScore); + R.Route(`player24.write`, write); +} + +const getInfoCommon = (req: EamuseInfo) => { + const result: any = { + phase: [], + choco: [], + goods: [], + area: [], + }; + + // Phase + const date: number = parseInt(req.model.match(/:(\d*)$/)[1]); + let phaseData: Phase[] = PHASE[getVersion(req)]; + + for (const phase of phaseData) { + result.phase.push({ + event_id: K.ITEM('s16', phase.id), + phase: K.ITEM('s16', phase.p), + }); + } + + // Choco + for (let i = 1; i <= 5; ++i) { + result.choco.push({ + choco_id: K.ITEM('s16', i), + param: K.ITEM('s32', -1), + }); + } + + // Goods + for (let i = 1; i <= 98; ++i) { + let price = 200; + if (i < 15) { + price = 30; + } else if (i < 30) { + price = 40; + } else if (i < 45) { + price = 60; + } else if (i < 60) { + price = 80; + } + + result.goods.push({ + item_id: K.ITEM('s32', i), + item_type: K.ITEM('s16', 3), + price: K.ITEM('s32', price), + goods_type: K.ITEM('s16', 0), + }); + } + + // Area + for (let i = 1; i <= 16; ++i) { + result.area.push({ + area_id: K.ITEM('s16', i), + end_date: K.ITEM('u64', BigInt(0)), + medal_id: K.ITEM('s16', i), + is_limit: K.ITEM('bool', 0), + }); + } + + // TODO : Course ranking + // TODO : Most popular characters + // TODO : Most popular music + + return result; +} + +const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + return send.object(getInfoCommon(req)); +} + +const start = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const result = { + play_id: K.ITEM('s32', 1), + ...getInfoCommon(req), + }; + await send.object(result); +}; + +/** + * Create a new profile and send it. + */ +const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const name = $(data).str('name'); + + send.object(await getProfile(refid, getVersion(req), name)); +}; + +/** + * Read a profile and send it. + */ +const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + send.object(await getProfile(refid, getVersion(req))); +}; + +const buy = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const type = $(data).number('type', -1); + const id = $(data).number('id', -1); + const param = $(data).number('param', 0); + const price = $(data).number('price', 0); + const lumina = $(data).number('lumina', 0); + + if (type < 0 || id < 0) { + return send.deny(); + } + + if (lumina >= price) { + const version = getVersion(req); + + const params = await utils.readParams(refid, version); + params.params.player_point = lumina - price; + await utils.writeParams(refid, version, params); + + const achievements = await utils.readAchievements(refid, version, {...defaultAchievements, version}); + achievements.items[`${type}:${id}`] = param; + await utils.writeAchievements(refid, version, achievements); + } + send.success(); +}; + +const readScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + const version = getVersion(req); + if (!refid) return send.deny(); + + send.object({ music: await getScores(refid, version) }); +}; + +const getScores = async (refid: string, version: string) => { + const scoresData = await utils.readScores(refid, version); + const result = []; + + for (const key in scoresData.scores) { + const keyData = key.split(':'); + const score = scoresData.scores[key]; + const music = parseInt(keyData[0], 10); + const sheet = parseInt(keyData[1], 10); + + if (music > GAME_MAX_MUSIC_ID[version]) { + continue; + } + if ([0, 1, 2, 3].indexOf(sheet) == -1) { + continue; + } + + result.push({ + music_num: K.ITEM('s16', music), + sheet_num: K.ITEM('u8', sheet), + score: K.ITEM('s32', score.score), + clear_type: K.ITEM('u8', { + 100: 1, + 200: 2, + 300: 3, + 400: 4, + 500: 5, + 600: 6, + 700: 7, + 800: 8, + 900: 9, + 1000: 10, + 1100: 11, + }[score.clear_type]), + clear_rank: K.ITEM('u8', getRank(score.score)), + cnt: K.ITEM('s16', score.cnt), + }); + } + + return result; +}; + +const getRank = (score: number): number => { + if (score < 50000) { + return 1 + } else if (score < 62000) { + return 2 + } else if (score < 72000) { + return 3 + } else if (score < 82000) { + return 4 + } else if (score < 90000) { + return 5 + } else if (score < 95000) { + return 6 + } else if (score < 98000) { + return 7 + } + return 8 +} + +const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const version = getVersion(req); + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const music = $(data).number('music_num'); + const sheet = $(data).number('sheet_num'); + const clear_type = { + 1: 100, + 2: 200, + 3: 300, + 4: 400, + 5: 500, + 6: 600, + 7: 700, + 8: 800, + 9: 900, + 10: 1000, + 11: 1100, + }[$(data).number('clear_type')]; + const score = $(data).number('score'); + + const key = `${music}:${sheet}`; + + const scoresData = await utils.readScores(refid, version, true); + if (!scoresData.scores[key]) { + scoresData.scores[key] = { + score, + cnt: 1, + clear_type + }; + } else { + scoresData.scores[key] = { + score: Math.max(score, scoresData.scores[key].score), + cnt: scoresData.scores[key].cnt + 1, + clear_type: Math.max(clear_type, scoresData.scores[key].clear_type || 0) + }; + } + + utils.writeScores(refid, version, scoresData); + + send.success(); +}; + +/** + * Get/create the profile based on refid + * @param refid the profile refid + * @param name if defined, create/update the profile with the given name + * @returns + */ +const getProfile = async (refid: string, version: string, name?: string) => { + const profile = await utils.readProfile(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + const achievements = await utils.readAchievements(refid, version, {...defaultAchievements, version}); + + let player: any = { + result: K.ITEM('s8', 0), + account: { + name: K.ITEM('str', profile.name), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + staff: K.ITEM('s8', 0), + item_type: K.ITEM('s16', 0), + item_id: K.ITEM('s16', 0), + is_conv: K.ITEM('s8', 0), + license_data: K.ARRAY('s16', Array(20).fill(-1)), + + // TODO: replace with real data + total_play_cnt: K.ITEM('s16', 100), + today_play_cnt: K.ITEM('s16', 50), + consecutive_days: K.ITEM('s16', 365), + total_days: K.ITEM('s16', 366), + interval_day: K.ITEM('s16', 1), + + // TODO: replace with real data + my_best: K.ARRAY('s16', Array(10).fill(-1)), + latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), + active_fr_num: K.ITEM('u8', 0), + }, + netvs: { + record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), + dialog: [ + K.ITEM('str', 'dialog'), + K.ITEM('str', 'dialog'), + K.ITEM('str', 'dialog'), + K.ITEM('str', 'dialog'), + K.ITEM('str', 'dialog'), + K.ITEM('str', 'dialog'), + ], + ojama_condition: K.ARRAY('s8', Array(74).fill(0)), + set_ojama: K.ARRAY('s8', [0, 0, 0]), + set_recommend: K.ARRAY('s8', [0, 0, 0]), + netvs_play_cnt: K.ITEM('u32', 0), + }, + eaappli: { + relation: K.ITEM('s8', -1), + }, + custom_cate: { + valid: K.ITEM('s8', 0), + lv_min: K.ITEM('s8', -1), + lv_max: K.ITEM('s8', -1), + medal_min: K.ITEM('s8', -1), + medal_max: K.ITEM('s8', -1), + friend_no: K.ITEM('s8', -1), + score_flg: K.ITEM('s8', -1), + }, + // TODO: Navi data ?? + navi_data: { + raisePoint: K.ARRAY('s32', [-1, -1, -1, -1, -1]), + navi_param: { + navi_id: K.ITEM('u16', 0), + friendship: K.ITEM('s32', 0), + }, + }, + // TODO: Daily missions + mission: [ + { + mission_id: K.ITEM('u32', 170), + gauge_point: K.ITEM('u32', 0), + mission_comp: K.ITEM('u32', 0), + }, + { + mission_id: K.ITEM('u32', 157), + gauge_point: K.ITEM('u32', 0), + mission_comp: K.ITEM('u32', 0), + }, + { + mission_id: K.ITEM('u32', 47), + gauge_point: K.ITEM('u32', 0), + mission_comp: K.ITEM('u32', 0), + }, + ], + music: await getScores(refid, version), + area: [], + course_data: [], + fes: [], + item: [], + chara_param: [], + stamp: [], + }; + + const profileCharas = achievements.charas || {}; + for (const chara_id in profileCharas) { + player.chara_param.push({ + chara_id: K.ITEM('u16', parseInt(chara_id, 10)), + friendship: K.ITEM('u16', profileCharas[chara_id]), + }); + } + + const profileStamps = achievements.stamps || { '0': 0 }; + for (const stamp_id in profileStamps) { + player.stamp.push({ + stamp_id: K.ITEM('s16', parseInt(stamp_id, 10)), + cnt: K.ITEM('s16', profileStamps[stamp_id]), + }); + } + + const profileAreas = achievements.areas || {}; + for (const area_id in profileAreas) { + const area = profileAreas[area_id]; + player.area.push({ + area_id: K.ITEM('u32', parseInt(area_id, 10)), + chapter_index: K.ITEM('u8', area.chapter_index), + gauge_point: K.ITEM('u16', area.gauge_point), + is_cleared: K.ITEM('bool', area.is_cleared), + diary: K.ITEM('u32', area.diary), + }); + } + + const profileCourses = achievements.courses || {}; + for (const course_id in profileCourses) { + const course = profileCourses[course_id]; + player.course_data.push({ + course_id: K.ITEM('s16', parseInt(course_id, 10)), + clear_type: K.ITEM('u8', course.clear_type), + clear_rank: K.ITEM('u8', course.clear_rank), + total_score: K.ITEM('s32', course.total_score), + update_count: K.ITEM('s32', course.update_count), + sheet_num: K.ITEM('u8', course.sheet_num), + }); + } + + const profileFes = achievements.fes || {}; + for (const fes_id in profileFes) { + const fesElt = profileFes[fes_id]; + player.fes.push({ + fes_id: K.ITEM('u32', parseInt(fes_id, 10)), + chapter_index: K.ITEM('u8', fesElt.chapter_index), + gauge_point: K.ITEM('u16', fesElt.gauge_point), + is_cleared: K.ITEM('bool', fesElt.is_cleared), + }); + } + + const profileItems = achievements.items || {}; + for (const key in profileItems) { + const keyData = key.split(':'); + const type = parseInt(keyData[0], 10); + const id = parseInt(keyData[1], 10); + + const item: any = { + type: K.ITEM('u8', type), + id: K.ITEM('u16', id), + param: K.ITEM('u16', profileItems[key]), + is_new: K.ITEM('bool', 0), + get_time: K.ITEM('u64', BigInt(0)), + }; + + player.item.push(item); + } + + // Add version specific datas + let params = await utils.readParams(refid, version); + utils.addExtraData(player, params, EXTRA_DATA); + + return player; +} + +const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + const version = getVersion(req); + + const params = await utils.readParams(refid, version); + const achievements = await utils.readAchievements(refid, version, {...defaultAchievements, version}); + + utils.getExtraData(data, params, EXTRA_DATA); + + // areas + let areas = _.get(data, 'area', []); + if (!achievements.areas) { + achievements.areas = {}; + } + + if (!_.isArray(areas)) { + areas = [areas]; + } + + for (const area of areas) { + const id = $(area).number('area_id'); + const chapter_index = $(area).number('chapter_index'); + const gauge_point = $(area).number('gauge_point'); + const is_cleared = $(area).bool('is_cleared'); + const diary = $(area).number('diary'); + + achievements.areas[id] = { + chapter_index, + gauge_point, + is_cleared, + diary, + }; + } + + // courses + let courses = _.get(data, 'course_data', []); + if (!achievements.courses) { + achievements.courses = {}; + } + + if (!_.isArray(courses)) { + courses = [courses]; + } + + for (const course of courses) { + const id = $(course).number('course_id'); + const clear_type = $(course).number('clear_type'); + const clear_rank = $(course).number('clear_rank'); + const total_score = $(course).number('total_score'); + const update_count = $(course).number('update_count'); + const sheet_num = $(course).number('sheet_num'); + + achievements.courses[id] = { + clear_type, + clear_rank, + total_score, + update_count, + sheet_num, + }; + } + + // fes + let fes = _.get(data, 'fes', []); + if (!achievements.fes) { + achievements.fes = {}; + } + + if (!_.isArray(fes)) { + fes = [fes]; + } + + for (const fesElt of fes) { + const id = $(fesElt).number('fes_id'); + const chapter_index = $(fesElt).number('chapter_index'); + const gauge_point = $(fesElt).number('gauge_point'); + const is_cleared = $(fesElt).bool('is_cleared'); + + achievements.fes[id] = { + chapter_index, + gauge_point, + is_cleared, + }; + } + + // items + let items = _.get(data, 'item', []); + if (!achievements.items) { + achievements.items = {}; + } + + if (!_.isArray(items)) { + items = [items]; + } + + for (const item of items) { + const type = $(item).number('type'); + const id = $(item).number('id'); + const param = $(item).number('param'); + + const key = `${type}:${id}`; + + achievements.items[key] = param; + } + + // charas + let charas = _.get(data, 'chara_param', []); + if (!achievements.charas) { + achievements.charas = {}; + } + + if (!_.isArray(charas)) { + charas = [charas]; + } + + for (const chara of charas) { + const id = $(chara).number('chara_id'); + const param = $(chara).number('friendship'); + + achievements.charas[id] = param; + } + + // stamps + let stamps = _.get(data, 'stamp', []); + if (!achievements.stamps) { + achievements.stamps = { '0': 0 }; + } + + if (!_.isArray(stamps)) { + stamps = [stamps]; + } + + for (const stamp of stamps) { + const id = $(stamp).number('stamp_id'); + const cnt = $(stamp).number('cnt'); + + achievements.stamps[id] = cnt; + } + + await utils.writeParams(refid, version, params); + await utils.writeAchievements(refid, version, achievements); + + send.success(); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +const getVersion = (req: EamuseInfo): string => { + const date: number = parseInt(req.model.match(/:(\d*)$/)[1]); + if (date >= 2018101700) { + return 'v25'; + } else { + return 'v24'; + } +} + +const GAME_MAX_MUSIC_ID = { + v24: 1704, + v25: 1877 +} + +const defaultAchievements: AchievementsUsaneko = { + collection: 'achievements', + version: null, + areas: {}, + courses: {}, + fes: {}, + items: {}, + charas: {}, + stamps: {}, +} + +const PHASE = { + v24: [ + { id: 0, p: 11 }, // Default song phase availability (0-11) + { id: 1, p: 2 }, + { id: 2, p: 2 }, + { id: 3, p: 4 }, + { id: 4, p: 1 }, + { id: 5, p: 0 }, // Enable Net Taisen (0-1) + { id: 6, p: 1 }, // Enable NAVI-kun shunkyoku toujou, allows song 1608 to be unlocked (0-1) + { id: 7, p: 1 }, + { id: 8, p: 2 }, + { id: 9, p: 0 }, // Daily Mission (0-2) + { id: 10, p: 15 }, // NAVI-kun Song phase availability (0-15) + { id: 11, p: 1 }, + { id: 12, p: 2 }, + { id: 13, p: 1 }, // Enable Pop'n Peace preview song (0-1) + ], + v25: [ + { id: 0, p: 23 }, + { id: 1, p: 4 }, + { id: 2, p: 2 }, + { id: 3, p: 4 }, + { id: 4, p: 1 }, + { id: 5, p: 0 }, // Enable Net Taisen (0-1) + { id: 6, p: 1 }, + { id: 7, p: 1 }, + { id: 8, p: 2 }, + { id: 9, p: 0 }, // Daily Mission (0-2) + { id: 10, p: 30 }, + { id: 11, p: 1 }, + { id: 12, p: 2 }, + { id: 13, p: 1 }, + { id: 14, p: 39 }, + { id: 15, p: 2 }, + { id: 16, p: 3 }, + { id: 17, p: 8 }, + { id: 18, p: 1 }, + { id: 19, p: 1 }, + { id: 20, p: 13 }, + { id: 21, p: 20 }, // pop'n event archive + { id: 22, p: 2 }, + { id: 23, p: 1 }, + { id: 24, p: 1 }, + ] +} + +const EXTRA_DATA: ExtraData = { + + play_id: { type: 's32', path: 'account', default: 0 }, + start_type: { type: 's8', path: 'account', default: 0 }, + tutorial: { type: 's16', path: 'account', default: -1 }, + area_id: { type: 's16', path: 'account', default: 51 }, + read_news: { type: 's16', path: 'account', default: 0 }, + nice: { type: 's16', path: 'account', default: Array(30).fill(-1), isArray: true }, + favorite_chara: { type: 's16', path: 'account', default: Array(20).fill(-1), isArray: true }, + special_area: { type: 's16', path: 'account', default: Array(8).fill(-1), isArray: true }, + chocolate_charalist: { type: 's16', path: 'account', default: Array(5).fill(-1), isArray: true }, + chocolate_sp_chara: { type: 's32', path: 'account', default: 0 }, + chocolate_pass_cnt: { type: 's32', path: 'account', default: 0 }, + chocolate_hon_cnt: { type: 's32', path: 'account', default: 0 }, + chocolate_giri_cnt: { type: 's32', path: 'account', default: 0 }, + chocolate_kokyu_cnt: { type: 's32', path: 'account', default: 0 }, + teacher_setting: { type: 's16', path: 'account', default: Array(10).fill(-1), isArray: true }, + welcom_pack: { type: 'bool', path: 'account', default: 0 }, + use_navi: { type: 's16', path: 'account', default: 0 }, + ranking_node: { type: 's32', path: 'account', default: 0 }, + chara_ranking_kind_id: { type: 's32', path: 'account', default: 0 }, + navi_evolution_flg: { type: 's8', path: 'account', default: 0 }, + ranking_news_last_no: { type: 's32', path: 'account', default: 0 }, + power_point: { type: 's32', path: 'account', default: 0 }, + player_point: { type: 's32', path: 'account', default: 300 }, + power_point_list: { type: 's32', path: 'account', default: [0], isArray: true }, + + mode: { type: 'u8', path: 'config', default: 0 }, + chara: { type: 's16', path: 'config', default: 0 }, + music: { type: 's16', path: 'config', default: 0 }, + sheet: { type: 'u8', path: 'config', default: 0 }, + category: { type: 's8', path: 'config', default: 0 }, + sub_category: { type: 's8', path: 'config', default: 0 }, + chara_category: { type: 's8', path: 'config', default: 0 }, + ms_banner_disp: { type: 's8', path: 'config', default: 0 }, + ms_down_info: { type: 's8', path: 'config', default: 0 }, + ms_side_info: { type: 's8', path: 'config', default: 0 }, + ms_raise_type: { type: 's8', path: 'config', default: 0 }, + ms_rnd_type: { type: 's8', path: 'config', default: 0 }, + banner_sort: { type: 's8', path: 'config', default: 0 }, + course_id: { type: 's16', path: 'config', default: 0 }, + course_folder: { type: 's8', path: 'config', default: 0 }, + + hispeed: { type: 's16', path: 'option', default: 10 }, + popkun: { type: 'u8', path: 'option', default: 0 }, + hidden: { type: 'bool', path: 'option', default: 0 }, + hidden_rate: { type: 's16', path: 'option', default: -1 }, + sudden: { type: 'bool', path: 'option', default: 0 }, + sudden_rate: { type: 's16', path: 'option', default: -1 }, + randmir: { type: 's8', path: 'option', default: 0 }, + gauge_type: { type: 's8', path: 'option', default: 0 }, + ojama_0: { type: 'u8', path: 'option', default: 0 }, + ojama_1: { type: 'u8', path: 'option', default: 0 }, + forever_0: { type: 'bool', path: 'option', default: 0 }, + forever_1: { type: 'bool', path: 'option', default: 0 }, + full_setting: { type: 'bool', path: 'option', default: 0 }, + guide_se: { type: 's8', path: 'option', default: 0 }, + judge: { type: 'u8', path: 'option', default: 0 }, + + ep: { type: 'u16', path: 'info', default: 0 }, + + effect_left: { type: 'u16', path: 'customize', default: 0 }, + effect_center: { type: 'u16', path: 'customize', default: 0 }, + effect_right: { type: 'u16', path: 'customize', default: 0 }, + hukidashi: { type: 'u16', path: 'customize', default: 0 }, + comment_1: { type: 'u16', path: 'customize', default: 0 }, + comment_2: { type: 'u16', path: 'customize', default: 0 }, +} \ No newline at end of file diff --git a/popn@asphyxia/handler/utils.ts b/popn@asphyxia/handler/utils.ts index 46d083a..a742bdd 100644 --- a/popn@asphyxia/handler/utils.ts +++ b/popn@asphyxia/handler/utils.ts @@ -1,4 +1,218 @@ -export const getVersion = (info: EamuseInfo) => { - const moduleName: string = info.module; - return `v${moduleName.match(/[0-9]+/)[0]}`; -}; \ No newline at end of file +import { Achievements } from "../models/achievements"; +import { Profile, Scores, ExtraData, Params } from "../models/common"; + +const CURRENT_DATA_VERSION = 2; + +export const addExtraData = (player: any, params: Params, extraData: ExtraData) => { + for (const field in extraData) { + const fieldName = field.replace(/(__\d*)/, ''); + + const fieldMetaData = extraData[field]; + if (fieldMetaData.isArray) { + _.set( + player, + `${fieldMetaData.path}.${fieldName}`, + K.ARRAY( + fieldMetaData.type as any, + _.get(params, `params.${field}`, fieldMetaData.default) + ) + ); + } else { + _.set( + player, + `${fieldMetaData.path}.${fieldName}`, + K.ITEM( + fieldMetaData.type as any, + _.get(params, `params.${field}`, fieldMetaData.default) + ) + ); + } + } +} + +export const getExtraData = (data: any, params: Params, extraData: ExtraData) => { + for (const field in extraData) { + const fieldName = field.replace(/(__\d*)/, ''); + const fieldMetaData = extraData[field]; + + let path = fieldMetaData.path; + if (fieldMetaData.pathSrc != undefined) { + path = fieldMetaData.pathSrc; + } + if (path.length > 0) { + path += '.'; + } + + let value = _.get(data, path + fieldName + '.@content'); + if (value == 'undefined' && value == null) { + continue; + } + + if (_.isArray(value) && value.length == 1) { + value = value[0]; + } + + _.set(params, `params.${field}`, value); + } +} + +export const readProfile = async (refid: string): Promise => { + const profile = await DB.FindOne(refid, { collection: 'profile' }); + if (profile !== undefined && profile !== null && profile.dataVersion !== CURRENT_DATA_VERSION) { + return await doConvert(profile); + } + return profile || { collection: 'profile', name: 'ゲスト', dataVersion: CURRENT_DATA_VERSION }; +} + +export const writeProfile = async (refid: string, profile: Profile) => { + await DB.Upsert(refid, { collection: 'profile' }, profile); +} + +export const readParams = async (refid: string, version: string): Promise => { + const params = await DB.FindOne(refid, { collection: 'params', version }); + return params || { collection: 'params', version, params: {} }; +} + +export const writeParams = async (refid: string, version: string, params: Params) => { + await DB.Upsert(refid, { collection: 'params', version }, params); +} + +export const readScores = async (refid: string, version: string, forceVersion: boolean = false): Promise => { + if (forceVersion || !U.GetConfig("enable_score_sharing")) { + const score = await DB.FindOne(refid, { collection: 'scores', version }); + return score || { collection: 'scores', version, scores: {} }; + } else { + let retScore = { collection: 'scores', version, scores: {} }; + const scores = await DB.Find(refid, { collection: 'scores' }); + for (const score of scores) { + _.mergeWith(retScore.scores, score.scores, (objValue, srcValue) => { + if (objValue == undefined && srcValue != undefined) { + return srcValue; + } + return { + score: Math.max(objValue.score, srcValue.score), + cnt: objValue.cnt + srcValue.cnt, + clear_type: Math.max(objValue.clear_type, srcValue.clear_type) + } + }); + } + return retScore; + } +} + +export const writeScores = async (refid: string, version: string, scores: Scores) => { + await DB.Upsert(refid, { collection: 'scores', version }, scores); +} + +export const readAchievements = async (refid: string, version: string, defaultValue: Achievements): Promise => { + const achievements = await DB.FindOne(refid, { collection: 'achievements', version }); + return achievements || defaultValue; +} + +export const writeAchievements = async (refid: string, version: string, achievements: Achievements) => { + await DB.Upsert(refid, { collection: 'achievements', version }, achievements); +} + +const doConvert = async (profile: ProfileDoc): Promise> => { + let achievements = []; + + // charas + if (profile.charas !== undefined) { + for (let version in profile.charas) { + achievements[version] = { collection: 'achievements', version, charas: profile.charas[version] }; + } + } + + // stamps + if (profile.stamps !== undefined) { + for (let version in profile.stamps) { + if (achievements[version] === undefined) { + achievements[version] = { collection: 'achievements', version, stamps: profile.stamps[version] }; + } else { + achievements[version].stamps = profile.stamps[version] + } + } + } + + // medals + if (profile.medals !== undefined) { + for (let version in profile.medals) { + if (achievements[version] === undefined) { + achievements[version] = { collection: 'achievements', version, medals: profile.medals[version] }; + } else { + achievements[version].medals = profile.medals[version] + } + } + } + + // items + if (profile.items !== undefined) { + for (let version in profile.items) { + if (achievements[version] === undefined) { + achievements[version] = { collection: 'achievements', version, items: profile.items[version] }; + } else { + achievements[version].items = profile.items[version] + } + } + } + + // Write achievements + for (let version in achievements) { + const nbAchievements = await DB.Count(profile.__refid, { collection: 'achievements', version }); + if (nbAchievements == 0) { + await DB.Insert(profile.__refid, achievements[version]); + } + } + + // Write extras/params + if (profile.extras !== undefined) { + for (let version in profile.extras) { + const nbParams = await DB.Count(profile.__refid, { collection: 'params', version }); + if (nbParams == 0) { + let params: Params = { collection: 'params', version, params: profile.extras[version] }; + + // stamps + if (profile.stamps !== undefined && profile.stamps[version] !== undefined) { + const key = Object.keys(profile.stamps[version])[0]; + params.params.stamp_id = key; + params.params.cnt = profile.stamps[version][key]; + } + + await DB.Insert(profile.__refid, params); + } + } + } + + // Update profile + const newProfile = await (await DB.Upsert(profile.__refid, { collection: 'profile' }, { collection: 'profile', name: profile.name, dataVersion: 2 })).docs[0]; + + // Update scores + let scoresData: Scores = { collection: 'scores', version: 'v25', scores: {} }; + const oldScores = await DB.Find(null, { collection: 'scores' }); + for (const oldScore of oldScores) { + for (const key in oldScore.scores) { + scoresData.scores[key] = { + score: oldScore.scores[key].score, + cnt: oldScore.scores[key].cnt, + clear_type: { + 0: 100, + 1: 100, + 2: 200, + 3: 300, + 4: 400, + 5: 500, + 6: 600, + 7: 700, + 8: 800, + 9: 900, + 10: 1000, + 11: 1100, + }[Math.max(oldScore.scores[key].clearmedal || 0, oldScore.scores[key].clear_type || 0)] + }; + } + await DB.Remove(oldScore.__refid, { collection: 'scores' }); + await DB.Insert(oldScore.__refid, scoresData); + } + + return newProfile; +} \ No newline at end of file diff --git a/popn@asphyxia/handler/webui.ts b/popn@asphyxia/handler/webui.ts index 139ccd1..5f93701 100644 --- a/popn@asphyxia/handler/webui.ts +++ b/popn@asphyxia/handler/webui.ts @@ -1,5 +1,4 @@ -import { Profile } from "../models/profile"; -import { Scores } from "../models/scores"; +import { Profile, Scores } from "../models/common"; export const importPnmData = async (data: { refid: string; diff --git a/popn@asphyxia/index.ts b/popn@asphyxia/index.ts index 2b405a3..12a6877 100644 --- a/popn@asphyxia/index.ts +++ b/popn@asphyxia/index.ts @@ -1,43 +1,55 @@ -import { getInfo } from "./handler/common"; -import { newPlayer, read, readScore, start, writeMusic, write, buy } from "./handler/player"; +import * as tunestreet from "./handler/tunestreet"; +import * as fantasia from "./handler/fantasia"; +import * as sunny from "./handler/sunny"; +import * as lapistoria from "./handler/lapistoria"; +import * as eclale from "./handler/eclale"; +import * as usaneko from "./handler/usaneko"; import { importPnmData } from "./handler/webui"; +const getVersion = (req: any) => { + switch (req.gameCode) { + case 'K39': + return tunestreet; + case 'L39': + return fantasia; + case 'M39': + return sunny; + } +} + export function register() { + R.GameCode('K39'); + R.GameCode('L39'); R.GameCode('M39'); - - R.Config("enable_25_event", { - name: "PNM25 event", - desc: "Enable the pop'n event archive", + + R.Config("enable_score_sharing", { + name: "Score sharing", + desc: "Enable sharing scores between versions", type: "boolean", default: true, }); R.WebUIEvent('importPnmData', importPnmData); + R.WebUIEvent('updatePnmPlayerInfo', async (data: any) => { await DB.Update(data.refid, { collection: 'profile' }, { $set: { name: data.name } }); }); - const PlayerRoute = (method: string, handler: EPR | boolean) => { - R.Route(`player24.${method}`, handler); - R.Route(`player23.${method}`, handler); - }; + // Route management for PnM <= 21 - const CommonRoute = (method: string, handler: EPR | boolean) => { - R.Route(`info24.${method}`, handler); - R.Route(`info23.${method}`, handler); - }; + R.Route(`game.get`, async (req, data, send) => getVersion(req).getInfo(req, data, send)); + R.Route(`playerdata.new`, async (req, data, send) => getVersion(req).newPlayer(req, data, send)); + R.Route(`playerdata.conversion`, async (req, data, send) => getVersion(req).newPlayer(req, data, send)); + R.Route(`playerdata.get`, async (req, data, send) => getVersion(req).read(req, data, send)); + R.Route(`playerdata.set`, async (req, data, send) => getVersion(req).write(req, data, send)); - // Common - CommonRoute('common', (req, data, send) => { - return send.object(getInfo(req)); + // For Pnm >= 22, each game set his own route + + lapistoria.setRoutes(); + eclale.setRoutes(); + usaneko.setRoutes(); + + R.Unhandled((req: EamuseInfo, data: any, send: EamuseSend) => { + return send.success(); }); - - // Player - PlayerRoute('new', newPlayer); - PlayerRoute('read', read); - PlayerRoute('read_score', readScore); - PlayerRoute('write_music', writeMusic); - PlayerRoute('write', write); - PlayerRoute('start', start); - PlayerRoute('buy', buy); } \ No newline at end of file diff --git a/popn@asphyxia/models/achievements.ts b/popn@asphyxia/models/achievements.ts new file mode 100644 index 0000000..5f508ec --- /dev/null +++ b/popn@asphyxia/models/achievements.ts @@ -0,0 +1,93 @@ +export interface Achievements { + collection: 'achievements', + version: string, +} + +export interface AchievementsLapistoria extends Achievements { + version: 'v22', + + achievements: { + [stamp_id: string]: number; + }; + + stories: { + [id: string]: { + chapter_id: number; + gauge_point: number; + is_cleared: boolean; + clear_chapter: number; + }; + }; + + items: { + [key: string]: number; + }; + + charas: { + [chara_id: string]: number; + }; +} + +export interface AchievementsEclale extends Achievements { + version: 'v23', + + medals: { + [id: string]: { + level: number; + exp: number; + set_count: number; + get_count: number; + }; + }; + + items: { + [key: string]: number; + }; + + charas: { + [chara_id: string]: number; + }; +} + +export interface AchievementsUsaneko extends Achievements { + version: 'v24' | 'v25', + + areas: { + [id: string]: { + chapter_index: number; + gauge_point: number; + is_cleared: boolean; + diary: number; + }; + }; + + courses: { + [id: string]: { + clear_type: number; + clear_rank: number; + total_score: number; + update_count: number; + sheet_num: number; + }; + }; + + fes: { + [id: string]: { + chapter_index: number; + gauge_point: number; + is_cleared: boolean; + }; + }; + + items: { + [key: string]: number; + }; + + charas: { + [chara_id: string]: number; + }; + + stamps: { + [stamp_id: string]: number; + }; +} \ No newline at end of file diff --git a/popn@asphyxia/models/common.ts b/popn@asphyxia/models/common.ts new file mode 100644 index 0000000..7bc4d54 --- /dev/null +++ b/popn@asphyxia/models/common.ts @@ -0,0 +1,42 @@ +export interface Phase { + id: number; + p: number; +} + +export interface ExtraData { + [field: string]: { + path: string; + pathSrc?: string; + type: string; + default: any; + isArray?: true; + }; +}; + +export interface Profile { + collection: 'profile', + name: string; + dataVersion: number; +} + +export interface Params { + collection: 'params', + version: string, + + params: { + [key: string]: any; + }; +} + +export interface Scores { + collection: 'scores', + version: string, + + scores: { + [key: string]: { + clear_type?: number; + score: number; + cnt: number; + }; + }; +} \ No newline at end of file diff --git a/popn@asphyxia/models/profile.ts b/popn@asphyxia/models/profile.ts deleted file mode 100644 index 2faf776..0000000 --- a/popn@asphyxia/models/profile.ts +++ /dev/null @@ -1,40 +0,0 @@ -export interface Profile { - collection: 'profile', - - name: string; - - stamps: { - [ver: string]: { - [stamp_id: string]: number; - }; - }; - - medals: { - [ver: string]: { - [id: string]: { - level: number; - exp: number; - set_count: number; - get_count: number; - }; - }; - }; - - items: { - [ver: string]: { - [key: string]: number; - }; - }; - - charas: { - [ver: string]: { - [chara_id: string]: number; - }; - }; - - extras: { - [ver: string]: { - [key: string]: any; - }; - }; -} \ No newline at end of file diff --git a/popn@asphyxia/models/scores.ts b/popn@asphyxia/models/scores.ts deleted file mode 100644 index 5472678..0000000 --- a/popn@asphyxia/models/scores.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface Scores { - collection: 'scores', - - scores: { - [key: string]: { - clearmedal?: number; - clear_type?: number; - clear_rank?: number; - score: number; - cnt: number; - }; - }; -} \ No newline at end of file