diff --git a/.gitignore b/.gitignore index 5f51ed9..7439c72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Editor configs .vscode +.idea # External modules node_modules diff --git a/popn@asphyxia/README.md b/popn@asphyxia/README.md index f0e605d..68c2547 100644 --- a/popn@asphyxia/README.md +++ b/popn@asphyxia/README.md @@ -1,13 +1,36 @@ # Pop'n Music -Plugin Version: **v1.1.0** +Plugin Version: **v2.1.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 + +Important : require minimum Asphyxia Core **v1.31** ## Changelog + +### 2.1.0 +* Add rivals support (except for Tune Street) +* Some fixes + +### 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 +* Net Taisen disabled on 24/25 (code not implemented) + #### 1.1.0 Update phase data : All versions are on latest phase. @@ -20,4 +43,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 a2ead0a..0000000 --- a/popn@asphyxia/handler/common.ts +++ /dev/null @@ -1,322 +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 }, - { 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 }, - { 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 }, - { 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') { - return getInfo24(PHASE25); - } 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..7540fed --- /dev/null +++ b/popn@asphyxia/handler/eclale.ts @@ -0,0 +1,576 @@ +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); + R.Route(`player23.friend`, friend); +} + +/** + * Return current state of the game (phase, good prices, etc...) + */ +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); +}; + +/** + * Handler for new profile + */ +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)); +}; + +/** + * Handler for existing profile + */ +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)); +}; + +/** + * Handler fo buying goods with lumina + */ +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(); +}; + +/** + * Handler for getting the user scores + */ +const readScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).str('ref_id'); + if (!refid) return send.deny(); + + send.object({ music: await getScores(refid) }); +}; + +/** + * Read the user scores and format them (profile/friend) + * @param refid ID of the user + * @param forFriend If true, format the output for friend request. + */ +const getScores = async (refid: string, forFriend: boolean = false) => { + 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); + const clearType = { + 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]; + + if (music > GAME_MAX_MUSIC_ID) { + continue; + } + if ([0, 1, 2, 3].indexOf(sheet) == -1) { + continue; + } + + if (forFriend) { + result.push(K.ATTR({ + music_num: music.toString(), + sheet_num: sheet.toString(), + score: score.score.toString(), + clearmedal: clearType.toString() + })); + } else { + 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', clearType), + cnt: K.ITEM('s16', score.cnt), + }); + } + } + return result; +}; + +/** + * Handler for saving the scores + */ +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 + */ +const getProfile = async (refid: string, name?: string) => { + const profile = await utils.readProfile(refid); + const rivals = await utils.readRivals(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + let myBest = Array(10).fill(-1); + const scores = await utils.readScores(refid, version, true); + if (Object.entries(scores.scores).length > 0) { + const playCount = new Map(); + for (const key in scores.scores) { + const keyData = key.split(':'); + const music = parseInt(keyData[0], 10); + playCount.set(music, (playCount.get(music) || 0) + scores.scores[key].cnt); + } + + const sortedPlayCount = new Map([...playCount.entries()].sort((a, b) => b[1] - a[1])); + let i = 0; + for (const value of sortedPlayCount.keys()) { + if (i >= 10) { + break; + } + myBest[i] = value; + i++; + } + } + + 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)), + active_fr_num: K.ITEM('u8', rivals.rivals.length), + + // 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', myBest), + 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), + }, + 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; +} + +/** + * Handler for saving the profile + */ +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(); +}; + +/** + * Handler for sending rivals + */ +const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const no = parseInt($(data).attr()['no'], 10); + + const rivals = await utils.readRivals(refid); + + if (no < 0 || no >= rivals.rivals.length) { + send.object({ result: K.ITEM('s8', 2) }); + return; + } + + const profile = await utils.readProfile(rivals.rivals[no]); + const params = await utils.readParams(rivals.rivals[no], version); + + const friend = { + friend: { + no: K.ITEM('s16', no), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + name: K.ITEM('str', profile.name), + chara: K.ITEM('s16', params.params.chara || -1), + is_open: K.ITEM('s8', 1), + music: await getScores(rivals.rivals[no], true), + } + } + + send.object(friend); +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +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..15c1a8e --- /dev/null +++ b/popn@asphyxia/handler/fantasia.ts @@ -0,0 +1,336 @@ +import { ExtraData } from "../models/common"; +import * as utils from "./utils"; + +/** + * Handler for getting the current state 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); +}; + +/** + * Handler for new profile + */ +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)); +}; + +/** + * Handler for existing profile + */ +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); + const rivals = await utils.readRivals(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + // Get Score + const scores = await getScores(refid); + let clear_medal_sub = Array(GAME_MAX_MUSIC_ID).fill(0); + + let myBest = Array(20).fill(-1); + const sortedPlayCount = new Map([...scores.playCount.entries()].sort((a, b) => b[1] - a[1])); + let i = 0; + for (const value of sortedPlayCount.keys()) { + if (i >= 20) { + break; + } + myBest[i] = value; + i++; + } + + 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), + my_best: K.ARRAY('s16', myBest), + clear_medal: K.ARRAY('u16', scores.clear_medal), + clear_medal_sub: K.ARRAY('u8', clear_medal_sub), + active_fr_num: K.ITEM('u8', rivals.rivals.length), + + // 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), + latest_music: K.ARRAY('s16', [-1, -1, -1]), + }, + 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(scores.hiscore_array)) + }; + + // Add version specific datas + const params = await utils.readParams(refid, version); + utils.addExtraData(player, params, extraData); + + player.player_card.best_music = K.ARRAY('s16', myBest.slice(0, 3)); + + return player; +} + +/** + * Handler for saving profile ans scores + */ +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); +}; + +/** + * Handler for sending rivals + */ +export const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const rivals = await utils.readRivals(refid); + let result = { + friend: [] + } + + for (const rival of rivals.rivals.slice(0, 2)) { + const profile = await utils.readProfile(rival); + const params = await utils.readParams(rival, version); + + const scores = await getScores(refid); + + result.friend.push({ + open: K.ITEM('s8', 1), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + name: K.ITEM('str', profile.name), + chara: K.ITEM('s16', params.params.chara || -1), + clear_medal: K.ARRAY('u16', scores.clear_medal), + hiscore: K.ITEM('bin', Buffer.from(scores.hiscore_array)) + }); + } + + send.object(result); +} + +/** + * Read the user scores and format them + * @param refid ID of the user + */ +const getScores = async (refid: string) => { + 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); + + const scoresData = await utils.readScores(refid, version); + const playCount = new Map(); + 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); + + playCount.set(music, (playCount.get(music) || 0) + score.cnt); + } + + return { + hiscore_array, + clear_medal, + playCount + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +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..659a298 --- /dev/null +++ b/popn@asphyxia/handler/lapistoria.ts @@ -0,0 +1,529 @@ +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); + R.Route(`player22.friend`, friend); +} + +/** + * Handler for getting current state of the game. + */ +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); +}; + +/** + * Handler for new profile + */ +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)); +}; + +/** + * Handler for existing profile + */ +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)); +}; + +/** + * Handler for saving the scores + */ +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 + */ +const getProfile = async (refid: string, name?: string) => { + const profile = await utils.readProfile(refid); + const rivals = await utils.readRivals(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]), + active_fr_num: K.ITEM('u8', rivals.rivals.length), + + // 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), + latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), + }, + 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); + const playCount = new Map(); + 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), + }); + + playCount.set(music, (playCount.get(music) || 0) + score.cnt); + } + + let myBest = Array(10).fill(-1); + const sortedPlayCount = new Map([...playCount.entries()].sort((a, b) => b[1] - a[1])); + let i = 0; + for (const value of sortedPlayCount.keys()) { + if (i >= 10) { + break; + } + myBest[i] = value; + i++; + } + player.account.my_best = K.ARRAY('s16', myBest); + + // 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; +} + +/** + * Handler for saving the profile + */ +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(); +}; + +/** + * Handler for sending rivals + */ +const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const no = parseInt($(data).attr()['no'], 10); + + const rivals = await utils.readRivals(refid); + + if (no < 0 || no >= rivals.rivals.length) { + send.object({ result: K.ITEM('s8', 2) }); + return; + } + + const profile = await utils.readProfile(rivals.rivals[no]); + const params = await utils.readParams(rivals.rivals[no], version); + + // Add Score + const scoresData = await utils.readScores(rivals.rivals[no], version); + const scores = []; + 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; + } + + scores.push(K.ATTR({ + music_num: music.toString(), + sheet_num: sheet.toString(), + score: score.score.toString(), + clearmedal: { + 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].toString(), + })); + } + + const friend = { + friend: { + no: K.ITEM('s16', no), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + name: K.ITEM('str', profile.name), + chara: K.ITEM('s16', params.params.chara || -1), + is_open: K.ITEM('s8', 1), + music: scores, + } + } + + send.object(friend); +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +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..7dff458 --- /dev/null +++ b/popn@asphyxia/handler/sunny.ts @@ -0,0 +1,385 @@ +import { ExtraData } from "../models/common"; +import * as utils from "./utils"; + +/** + * Handler for getting the current state of the game (phase, good prices, etc...) + */ +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', Array(10).fill(-1)), + up_ranking: K.ARRAY('s16', Array(10).fill(-1)), + }; + + return send.object(result); +}; + +/** + * Handler for new profile + */ +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)); +}; + +/** + * Handler for existing profile + */ +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); + const rivals = await utils.readRivals(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + // Get Score + const scores = await getScores(refid); + let clear_medal_sub = Array(GAME_MAX_MUSIC_ID).fill(0); + + let myBest = Array(20).fill(-1); + const sortedPlayCount = new Map([...scores.playCount.entries()].sort((a, b) => b[1] - a[1])); + let i = 0; + for (const value of sortedPlayCount.keys()) { + if (i >= 20) { + break; + } + myBest[i] = value; + i++; + } + + 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), + my_best: K.ARRAY('s16', myBest), + clear_medal: K.ARRAY('u16', scores.clear_medal), + clear_medal_sub: K.ARRAY('u8', clear_medal_sub), + active_fr_num: K.ITEM('u8', rivals.rivals.length), + + // 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), + latest_music: K.ARRAY('s16', [-1, -1, -1]), + }, + 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(scores.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; +} + +/** + * Handler for saving profile and scores + */ +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); +}; + +/** + * Handler for sending rivals + */ +export const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const rivals = await utils.readRivals(refid); + let result = { + friend: [] + } + + for (const rival of rivals.rivals.slice(0, 2)) { + const profile = await utils.readProfile(rival); + const params = await utils.readParams(rival, version); + + const scores = await getScores(rival); + + result.friend.push({ + open: K.ITEM('s8', 1), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + name: K.ITEM('str', profile.name), + chara: K.ITEM('s16', params.params.chara || -1), + hair: K.ITEM('u8', params.params.hair || 0), + face: K.ITEM('u8', params.params.face || 0), + body: K.ITEM('u8', params.params.body || 0), + effect: K.ITEM('u8', params.params.effect || 0), + object: K.ITEM('u8', params.params.object || 0), + comment: K.ARRAY('u8', params.params.comment || [0, 0]), + clear_medal: K.ARRAY('u16', scores.clear_medal), + hiscore: K.ITEM('bin', Buffer.from(scores.hiscore_array)) + }); + } + + send.object(result); +} + +/** + * Read the user scores and format them + * @param refid ID of the user + */ +const getScores = async (refid: string) => { + 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); + + const scoresData = await utils.readScores(refid, version); + const playCount = new Map(); + 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); + + playCount.set(music, (playCount.get(music) || 0) + score.cnt); + } + + return { + hiscore_array, + clear_medal, + playCount + } +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +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..f63c937 --- /dev/null +++ b/popn@asphyxia/handler/tunestreet.ts @@ -0,0 +1,304 @@ +import * as utils from "./utils"; + +/** + * Handler for getting the current state of the game (phase, good prices, etc...) + */ +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); +}; + +/** + * Handler for new profile + */ +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)); +}; + +/** + * Handler for existing profile + */ +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 and scores based on refid + * @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); + } + + const params = await utils.readParams(refid, version); + + let binary_profile = Array(2200).fill(0); + let name_binary = U.EncodeString(profile.name, 'shift_jis'); + for (let i = 0; i < name_binary.length || i < 12; i++) { + binary_profile[i] = name_binary[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); + const playCount = new Map(); + 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); + + playCount.set(music, (playCount.get(music) || 0) + score.cnt); + } + + let myBest = Array(20).fill(-1); + const sortedPlayCount = new Map([...playCount.entries()].sort((a, b) => b[1] - a[1])); + let i = 0; + for (const value of sortedPlayCount.keys()) { + if (i >= 20) { + break; + } + myBest[i] = value; + i++; + } + + let profile_pos = 68 + for (const musicid of myBest) { + binary_profile[profile_pos] = musicid & 0xFF + binary_profile[profile_pos + 1] = (musicid >> 8) & 0xFF + profile_pos = profile_pos + 2 + } + + 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 +} + +/** + * Handler for saving profile and scores + */ +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); +}; + +export const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + // No rivals support for Tune street :( + send.deny(); +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +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..95a5a4e --- /dev/null +++ b/popn@asphyxia/handler/usaneko.ts @@ -0,0 +1,808 @@ +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); + R.Route(`player24.friend`, friend); +} + +/** + * Return current state of the game (phase, good prices, etc...) + */ +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); +}; + +/** + * Handler for new profile + */ +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)); +}; + +/** + * Handler for existing profile + */ +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))); +}; + +/** + * Handler fo buying goods with lumina + */ +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(); +}; + +/** + * Handler for getting the user scores + */ +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) }); +}; + +/** + * Read the user scores and format them (profile/friend) + * @param refid ID of the user + * @param forFriend If true, format the output for friend request. + */ +const getScores = async (refid: string, version: string, forFriend: boolean = false) => { + 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); + const clearType = { + 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]; + + if (music > GAME_MAX_MUSIC_ID[version]) { + continue; + } + if ([0, 1, 2, 3].indexOf(sheet) == -1) { + continue; + } + + if (forFriend) { + result.push(K.ATTR({ + music_num: music.toString(), + sheet_num: sheet.toString(), + score: score.score.toString(), + cleartype: clearType.toString(), + clearrank: getRank(score.score).toString() + })); + } else { + 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', clearType), + clear_rank: K.ITEM('u8', getRank(score.score)), + cnt: K.ITEM('s16', score.cnt), + }); + } + } + + return result; +}; + +/** + * Return the rank based on the given score + */ +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 +} + +/** + * Handler for saving the scores + */ +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 + */ +const getProfile = async (refid: string, version: string, name?: string) => { + const profile = await utils.readProfile(refid); + const rivals = await utils.readRivals(refid); + + if (name && name.length > 0) { + profile.name = name; + await utils.writeProfile(refid, profile); + } + + let myBest = Array(10).fill(-1); + const scores = await utils.readScores(refid, version, true); + if (Object.entries(scores.scores).length > 0) { + const playCount = new Map(); + for (const key in scores.scores) { + const keyData = key.split(':'); + const music = parseInt(keyData[0], 10); + playCount.set(music, (playCount.get(music) || 0) + scores.scores[key].cnt); + } + + const sortedPlayCount = new Map([...playCount.entries()].sort((a, b) => b[1] - a[1])); + let i = 0; + for (const value of sortedPlayCount.keys()) { + if (i >= 10) { + break; + } + myBest[i] = value; + i++; + } + } + + 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)), + my_best: K.ARRAY('s16', myBest), + active_fr_num: K.ITEM('u8', rivals.rivals.length), + + // 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), + 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'), + 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 achievements = await utils.readAchievements(refid, version, { ...defaultAchievements, version }); + + 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]), + }); + } + + let profileStamps = achievements.stamps; + if (Object.entries(profileStamps).length == 0) { + profileStamps = { "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; +} + +/** + * Handler for saving the profile + */ +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(); +}; + +/** + * Handler for sending rivals + */ +const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const no = parseInt($(data).attr()['no'], 10); + const version = getVersion(req); + + const rivals = await utils.readRivals(refid); + + if (no < 0 || no >= rivals.rivals.length) { + send.object({ result: K.ITEM('s8', 2) }); + return; + } + + const profile = await utils.readProfile(rivals.rivals[no]); + const params = await utils.readParams(rivals.rivals[no], version); + + const friend = { + friend: { + no: K.ITEM('s16', no), + g_pm_id: K.ITEM('str', 'ASPHYXIAPLAY'), + name: K.ITEM('str', profile.name), + chara: K.ITEM('s16', params.params.chara || -1), + is_open: K.ITEM('s8', 1), + music: await getScores(rivals.rivals[no], version, true), + } + } + + send.object(friend); +} + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +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..53c7654 100644 --- a/popn@asphyxia/handler/utils.ts +++ b/popn@asphyxia/handler/utils.ts @@ -1,4 +1,223 @@ -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, Rivals } 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 readRivals = async (refid: string): Promise => { + const rivals = await DB.FindOne(refid, { collection: 'rivals' }); + return rivals || { collection: 'rivals', rivals: [] }; +} + +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 c5eb5bb..cb63096 100644 --- a/popn@asphyxia/index.ts +++ b/popn@asphyxia/index.ts @@ -1,33 +1,77 @@ -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"; +import { Rivals } from "./models/common"; + +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_score_sharing", { + name: "Score sharing", + desc: "Enable sharing scores between versions. This also affect rivals scores.", + type: "boolean", + default: true, + }); + R.WebUIEvent('importPnmData', importPnmData); - const PlayerRoute = (method: string, handler: EPR | boolean) => { - R.Route(`player24.${method}`, handler); - R.Route(`player23.${method}`, handler); - }; - - const CommonRoute = (method: string, handler: EPR | boolean) => { - R.Route(`info24.${method}`, handler); - R.Route(`info23.${method}`, handler); - }; - - // Common - CommonRoute('common', (req, data, send) => { - return send.object(getInfo(req)); + R.WebUIEvent('updatePnmPlayerInfo', async (data: any) => { + await DB.Update(data.refid, { collection: 'profile' }, { $set: { name: data.name } }); }); - // Player - PlayerRoute('new', newPlayer); - PlayerRoute('read', read); - PlayerRoute('read_score', readScore); - PlayerRoute('write_music', writeMusic); - PlayerRoute('write', write); - PlayerRoute('start', start); - PlayerRoute('buy', buy); + // Rivals UI management + R.WebUIEvent('deleteRival', async (data: any) => { + const rivals = await DB.FindOne(data.refid, { collection: 'rivals' }) || { collection: 'rivals', rivals: [] }; + const idx = rivals.rivals.indexOf(data.rivalid); + if (idx >= 0) { + rivals.rivals.splice(idx, 1); + await DB.Update(data.refid, { collection: 'rivals' }, rivals); + } + }); + + R.WebUIEvent('addRival', async (data: any) => { + const refid = data.refid.trim(); + const profile = await DB.FindOne(data.rivalid, { collection: 'profile' }); + if (profile != undefined && profile != null) { + const rivals = await DB.FindOne(refid, { collection: 'rivals' }) || { collection: 'rivals', rivals: [] }; + if (rivals.rivals.length < 4) { + rivals.rivals.push(data.rivalid); + await DB.Upsert(refid, { collection: 'rivals' }, rivals); + } + } + }); + + // Route management for PnM <= 21 + 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)); + R.Route(`playerdata.friend`, async (req, data, send) => getVersion(req).friend(req, data, send)); + + // 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(); + }); } \ 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..b6160f1 --- /dev/null +++ b/popn@asphyxia/models/common.ts @@ -0,0 +1,47 @@ +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 Rivals { + collection: 'rivals', + rivals: string[] +} + +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 diff --git a/popn@asphyxia/webui/profile_page.pug b/popn@asphyxia/webui/profile_page.pug index 49043ff..3cecda4 100644 --- a/popn@asphyxia/webui/profile_page.pug +++ b/popn@asphyxia/webui/profile_page.pug @@ -1,7 +1,67 @@ +//DATA// + profile: DB.FindOne(refid, { collection: 'profile' }) + rivals: DB.FindOne(refid, { collection: 'rivals' }) + div div.notification.is-success.is-hidden#import-success .field - label.label.is-small Data imported + label.label.is-small Data imported + .card + .card-header + p.card-header-title + span.icon + i.mdi.mdi-account-edit + | User Detail + .card-content + form(method="post" action="/emit/updatePnmPlayerInfo") + input(type="hidden" id="refid" name="refid" value=refid) + .field + label.label Name + .control + input.input(type="text" name="name" maxlength="6", value=profile.name) + .field + button.button.is-primary(type="submit") + span.icon + i.mdi.mdi-check + span Submit + .card + .card-header + p.card-header-title + span.icon + i.mdi.mdi-account-edit + | Rivals + .card-content + .columns.is-multiline + if rivals != null + - for (const rival of rivals.rivals) + form(method="post" action="/emit/deleteRival").column.is-4 + .box + input(type="hidden" id="refid" name="refid" value=refid) + input(type="hidden" id="rivalid" name="rivalid" value=rival) + .field + input.input(type="text" value=rival disabled="disabled") + .field + button.button.is-primary + span.icon + i.mdi.mdi-file-import-outline + span Delete + if rivals == null || rivals.rivals.length < 4 + form(method="post" action="/emit/addRival").column.is-4 + .box + input(type="hidden" id="refid" name="refid" value=refid) + .field + input.input(type="text" id="rivalid" name="rivalid" placeholder="Rival ID (ex. AAB56E7436549D83)") + .field + button.button.is-primary + span.icon + i.mdi.mdi-file-import-outline + span Add + div + label To add a rival, use the profile ID located on the POPN Profiles page. + div + label There is a limit of 4 rivals maximum (only the 2 firsts will be used for Sunny Park and lower). + div + label The score sharing option also affect scores get from rivals. .card .card-header p.card-header-title @@ -9,7 +69,6 @@ div i.mdi.mdi-account-edit | Import data .card-content - input(type="hidden" id="refid" name="refid" value=refid) .field label.label Import data from previous Asphyxia (non-core) .field @@ -19,6 +78,9 @@ div .field label.help.is-danger /!\ Please backup your savedata.db before importing data /!\ .field - button.button.is-primary#import-click Import + button.button.is-primary#import-click + span.icon + i.mdi.mdi-file-import-outline + span Import script(src="static/js/profile_page.js") \ No newline at end of file diff --git a/sdvx@asphyxia/README.md b/sdvx@asphyxia/README.md index ca4cb63..1c0c309 100644 --- a/sdvx@asphyxia/README.md +++ b/sdvx@asphyxia/README.md @@ -4,5 +4,6 @@ Plugin Version: **v1.1** Supported Versions: +- BOOTH - HEAVENLY HAVEN - VIVID WAVE diff --git a/sdvx@asphyxia/data/vvw.ts b/sdvx@asphyxia/data/vvw.ts index 5b0762b..2a104a6 100644 --- a/sdvx@asphyxia/data/vvw.ts +++ b/sdvx@asphyxia/data/vvw.ts @@ -13,6 +13,7 @@ export const EVENT5 = [ 'FACTORY\t10', 'CONTINUATION', 'APPEAL_CARD_GEN_NEW_PRICE', + 'APPEAL_CARD_UNLOCK\t0,30170914,0,30171014,0,30171116,0,30180201,0,30180607,0,30181206,0,30200326,0,30200611', 'FAVORITE_APPEALCARD_MAX\t100', 'FAVORITE_MUSIC_MAX\t500', 'EVENTDATE_APRILFOOL', diff --git a/sdvx@asphyxia/handlers/common.ts b/sdvx@asphyxia/handlers/common.ts index 2bfa4a9..a5b4d0b 100644 --- a/sdvx@asphyxia/handlers/common.ts +++ b/sdvx@asphyxia/handlers/common.ts @@ -6,6 +6,12 @@ export const common: EPR = async (info, data, send) => { let courses = []; let extend = []; + const version = parseInt(info.model.split(":")[4]); + + if (version <= 2013052900) { + return send.pugFile('templates/booth/common.pug'); + } + switch (info.method) { case 'sv4_common': { events = EVENT4; @@ -26,11 +32,13 @@ export const common: EPR = async (info, data, send) => { if (U.GetConfig('unlock_all_songs')) { for (let i = 1; i < 1700; ++i) { for (let j = 0; j < 5; ++j) { + songs.push({ music_id: K.ITEM('s32', i), music_type: K.ITEM('u8', j), limited: K.ITEM('u8', 3), }); + } } } diff --git a/sdvx@asphyxia/handlers/features.ts b/sdvx@asphyxia/handlers/features.ts index 371f8e2..fe5abec 100644 --- a/sdvx@asphyxia/handlers/features.ts +++ b/sdvx@asphyxia/handlers/features.ts @@ -1,17 +1,46 @@ -import { Profile } from '../models/profile'; -import { MusicRecord } from '../models/music_record'; -import { IDToCode, GetCounter } from '../utils'; -import { Mix } from '../models/mix'; +import {Profile} from '../models/profile'; +import {MusicRecord} from '../models/music_record'; +import {getVersion, IDToCode, GetCounter} from '../utils'; +import {Mix} from '../models/mix'; export const hiscore: EPR = async (info, data, send) => { const records = await DB.Find(null, { collection: 'music' }); + const version = getVersion(info); + const profiles = _.groupBy( await DB.Find(null, { collection: 'profile' }), '__refid' ); - send.object({ + if (version === 1) { + return send.object({ + hiscore: K.ATTR({ type: "1" }, { + music: _.map( + _.groupBy(records, r => { + return `${r.mid}:${r.type}`; + }), + r => _.maxBy(r, 'score') + ).map(r => (K.ATTR({ id: String(r.mid) }, { + note: (() => { + const notes = []; + + for (let i = 1; i <= 3; i++) { + if (r.type !== i) continue; + notes.push(K.ATTR({ type: String(r.type) }, { + name: K.ITEM('str', profiles[r.__refid][0].name), + score: K.ITEM('u32', r.score) + })) + } + + return notes; + })() + }))), + }) + }) + } + + return send.object({ sc: { d: _.map( _.groupBy(records, r => { @@ -40,7 +69,7 @@ export const rival: EPR = async (info, data, send) => { await DB.Find(null, { collection: 'profile' }) ).filter(p => p.__refid != refid); - send.object({ + return send.object({ rival: await Promise.all( rivals.map(async (p, index) => { return { @@ -84,7 +113,7 @@ export const saveMix: EPR = async (info, data, send) => { jacket: mix.number('jacket_id'), }); - send.object({ + return send.object({ automation: { mix_id: K.ITEM('s32', id), mix_code: K.ITEM('str', doc.code), @@ -105,11 +134,10 @@ export const loadMix: EPR = async (info, data, send) => { const mix = await DB.FindOne({ collection: 'mix', code }); if (!mix) { - send.object({ result: K.ITEM('s32', 1) }); - return; + return send.object({ result: K.ITEM('s32', 1) }); } - send.object({ + return send.object({ automation: { mix_id: K.ITEM('s32', mix.id), mix_code: K.ITEM('str', mix.code), diff --git a/sdvx@asphyxia/handlers/profiles.ts b/sdvx@asphyxia/handlers/profiles.ts index 0951e73..3e55e7d 100644 --- a/sdvx@asphyxia/handlers/profiles.ts +++ b/sdvx@asphyxia/handlers/profiles.ts @@ -1,18 +1,12 @@ -import { Skill } from '../models/skill'; -import { SDVX_AUTOMATION_SONGS } from '../data/vvw'; -import { Item } from '../models/item'; -import { Param } from '../models/param'; -import { MusicRecord } from '../models/music_record'; -import { CourseRecord } from '../models/course_record'; -import { Profile } from '../models/profile'; -import { IDToCode } from '../utils'; -import { Mix } from '../models/mix'; - -function getVersion(info: EamuseInfo) { - if (info.method.startsWith('sv4')) return 4; - if (info.method.startsWith('sv5')) return 5; - return 0; -} +import {Skill} from '../models/skill'; +import {SDVX_AUTOMATION_SONGS} from '../data/vvw'; +import {Item} from '../models/item'; +import {Param} from '../models/param'; +import {MusicRecord} from '../models/music_record'; +import {CourseRecord} from '../models/course_record'; +import {Profile} from '../models/profile'; +import {getVersion, IDToCode} from '../utils'; +import {Mix} from '../models/mix'; async function getAutomationMixes(params: Param[]) { const mixids = params @@ -30,12 +24,37 @@ function unlockNavigators(items: Partial[]) { } export const loadScore: EPR = async (info, data, send) => { - const refid = $(data).str('refid'); + const refid = $(data).str('refid', $(data).attr().dataid); if (!refid) return send.deny(); const records = await DB.Find(refid, { collection: 'music' }); - send.object({ + const version = getVersion(info); + + if (version === 1) { + return send.object({ + music: records.map(r => (K.ATTR({ music_id: String(r.mid) }, { + type: (() => { + const records = []; + + for (let i = 1; i <= 3; i++) { + if (r.type != i) continue; + records.push(K.ATTR({ + type_id: String(i), + score: String(r.score), + clear_type: String(r.clear), + score_grade: String(r.grade), + cnt: "0" + })); + } + + return records; + })() + }))) + }); + } + + return send.object({ music: { info: records.map(r => ({ param: K.ARRAY('u32', [ @@ -62,9 +81,57 @@ export const loadScore: EPR = async (info, data, send) => { }; export const saveScore: EPR = async (info, data, send) => { - const refid = $(data).str('refid'); + const refid = $(data).str('refid', $(data).attr().dataid); if (!refid) return send.deny(); + const version = getVersion(info); + + // Booth - Save score + if (version === 1) { + try { + const mid = parseInt($(data).attr().music_id); + const type = parseInt($(data).attr().music_type); + + if (_.isNil(mid) || _.isNil(type)) return send.deny(); + + const record = (await DB.FindOne(refid, { + collection: 'music', + mid, + type, + })) || { + collection: 'music', + mid, + type, + score: 0, + clear: 0, + grade: 0, + buttonRate: 0, + longRate: 0, + volRate: 0, + }; + + const score = $(data).attr().score ? parseInt($(data).attr().score) : 0; + const clear = $(data).attr().clear_type ? parseInt($(data).attr().clear_type) : 0; + const grade = $(data).attr().score_grade ? parseInt($(data).attr().score_grade) : 0; + if (score > record.score) { + record.score = score; + } + + record.clear = Math.max(clear, record.clear); + record.grade = Math.max(grade, record.grade); + + await DB.Upsert( + refid, + { collection: 'music', mid, type }, + record + ); + + return send.success(); + } catch { + return send.deny(); + } + } + const mid = $(data).number('music_id'); const type = $(data).number('music_type'); @@ -103,7 +170,7 @@ export const saveScore: EPR = async (info, data, send) => { record ); - send.success(); + return send.success(); }; export const saveCourse: EPR = async (info, data, send) => { @@ -134,16 +201,47 @@ export const saveCourse: EPR = async (info, data, send) => { } ); - send.success(); + return send.success(); }; export const save: EPR = async (info, data, send) => { - const refid = $(data).str('refid'); + const refid = $(data).str('refid', $(data).attr().refid); if (!refid) return send.deny(); const version = getVersion(info); if (version == 0) return send.deny(); + if (version === 1) { + try { + // Save Profile + await DB.Update( + refid, + { collection: 'profile' }, + { + $set: { + headphone: $(data).number('headphone'), + hiSpeed: $(data).number('hispeed'), + appeal: $(data).number('appeal_id'), + boothFrame: [$(data).number('frame0'), $(data).number('frame1'), $(data).number('frame2'), $(data).number('frame3'), $(data).number('frame4')], + musicID: parseInt($(data).attr("last").music_id), + musicType: parseInt($(data).attr("last").music_type), + sortType: parseInt($(data).attr("last").sort_type), + mUserCnt: $(data).number('m_user_cnt'), + }, + $inc: { + expPoint: $(data).number('gain_exp'), + packets: $(data).number('earned_gamecoin_packet'), + blocks: $(data).number('earned_gamecoin_block'), + }, + } + ); + + return send.success(); + } catch { + return send.deny(); + } + } + // Save Profile await DB.Update( refid, @@ -226,11 +324,11 @@ export const save: EPR = async (info, data, send) => { } ); - send.success(); + return send.success(); }; export const load: EPR = async (info, data, send) => { - const refid = $(data).str('refid'); + const refid = $(data).str('refid', $(data).attr().dataid); if (!refid) return send.deny(); const version = getVersion(info); @@ -241,8 +339,8 @@ export const load: EPR = async (info, data, send) => { }); if (!profile) { - send.object({ result: K.ITEM('u8', 1) }); - return; + if (version === 1) return send.object(K.ATTR({ none: "1" })); + return send.object({ result: K.ITEM('u8', 1) }); } let skill = (await DB.FindOne(refid, { @@ -253,16 +351,28 @@ export const load: EPR = async (info, data, send) => { const courses = await DB.Find(refid, { collection: 'course' }); const items = await DB.Find(refid, { collection: 'item' }); const params = await DB.Find(refid, { collection: 'param' }); - + let time = new Date(); + let tempHour = time.getHours(); + let tempDate = time.getDate(); + tempHour += 12; + tempDate += 1; + time.setDate(tempDate); + time.setHours(tempHour); + const currentTime = time.getTime(); const mixes = version == 5 ? await getAutomationMixes(params) : []; - send.pugFile('templates/load.pug', { + if (version === 1) { + return send.pugFile('templates/booth/load.pug', { code: IDToCode(profile.id), ...profile }); + } + + return send.pugFile('templates/load.pug', { courses, items: U.GetConfig('unlock_all_navigators') ? unlockNavigators(items) : items, params, skill, + currentTime, mixes, automation: version == 5 ? SDVX_AUTOMATION_SONGS : [], code: IDToCode(profile.id), @@ -271,10 +381,10 @@ export const load: EPR = async (info, data, send) => { }; export const create: EPR = async (info, data, send) => { - const refid = $(data).str('refid'); + const refid = $(data).str('refid', $(data).attr().refid); if (!refid) return send.deny(); - const name = $(data).str('name', 'GUEST'); + const name = $(data).str('name', $(data).attr().name ? $(data).attr().name : 'GUEST'); let id = _.random(0, 99999999); while (await DB.FindOne(null, { collecttion: 'profile', id })) { id = _.random(0, 99999999); @@ -306,10 +416,13 @@ export const create: EPR = async (info, data, send) => { musicID: 0, musicType: 0, sortType: 0, + expPoint: 0, + mUserCnt: 0, + boothFrame: [0, 0, 0, 0, 0] }; await DB.Upsert(refid, { collection: 'profile' }, profile); - send.object({ result: K.ITEM('u8', 0) }); + return send.object({ result: K.ITEM('u8', 0) }); }; export const buy: EPR = async (info, data, send) => { @@ -348,11 +461,11 @@ export const buy: EPR = async (info, data, send) => { ); } - send.object({ + return send.object({ gamecoin_packet: K.ITEM('u32', updated.docs[0].packets), gamecoin_block: K.ITEM('u32', updated.docs[0].blocks), }); } else { - send.success(); + return send.success(); } }; diff --git a/sdvx@asphyxia/index.ts b/sdvx@asphyxia/index.ts index 31a5dd1..0e23872 100644 --- a/sdvx@asphyxia/index.ts +++ b/sdvx@asphyxia/index.ts @@ -1,5 +1,5 @@ -import { common } from './handlers/common'; -import { hiscore, rival, saveMix, loadMix } from './handlers/features'; +import {common} from './handlers/common'; +import {hiscore, rival, saveMix, loadMix} from './handlers/features'; import { updateProfile, updateMix, @@ -29,6 +29,7 @@ export function register() { const MultiRoute = (method: string, handler: EPR | boolean) => { // Helper for register multiple versions. + R.Route(`game.${method}`, handler); R.Route(`game.sv4_${method}`, handler); R.Route(`game.sv5_${method}`, handler); }; @@ -53,15 +54,24 @@ export function register() { MultiRoute('load_ap', loadMix); // Lazy - MultiRoute('lounge', false); - MultiRoute('shop', true); + MultiRoute('lounge', (_, __, send) => send.object({ + interval: K.ITEM('u32', 30) + })); + MultiRoute('shop', (_, __, send) => send.object({ + nxt_time: K.ITEM('u32', 1000 * 5 * 60) + })); MultiRoute('save_e', true); MultiRoute('play_e', true); MultiRoute('play_s', true); MultiRoute('entry_s', true); MultiRoute('entry_e', true); MultiRoute('exception', true); - R.Route('eventlog.write', true); + R.Route('eventlog.write', (_, __, send) => send.object({ + gamesession: K.ITEM('s64', 1n), + logsendflg: K.ITEM('s32', 0), + logerrlevel: K.ITEM('s32', 0), + evtidnosendflg: K.ITEM('s32', 0) + })); R.Unhandled(); } diff --git a/sdvx@asphyxia/models/profile.ts b/sdvx@asphyxia/models/profile.ts index 045fb67..e10e995 100644 --- a/sdvx@asphyxia/models/profile.ts +++ b/sdvx@asphyxia/models/profile.ts @@ -11,6 +11,9 @@ export interface Profile { packets: number; blocks: number; + expPoint: number; + mUserCnt: number; + musicID: number; musicType: number; sortType: number; @@ -28,4 +31,6 @@ export interface Profile { effCLeft: number; effCRight: number; narrowDown: number; + + boothFrame: number[]; } diff --git a/sdvx@asphyxia/templates/booth/common.pug b/sdvx@asphyxia/templates/booth/common.pug new file mode 100644 index 0000000..6882290 --- /dev/null +++ b/sdvx@asphyxia/templates/booth/common.pug @@ -0,0 +1,16 @@ +- let music = 0; +- let event = 0; +- let catalog = 0; + +game + limited + while music < 200 + music(id=music++, flag=2) + + event + while event < 16 + info(id=event++) + + catalog + while catalog < 256 + info(id=catalog++, currency=1, price=1) diff --git a/sdvx@asphyxia/templates/booth/load.pug b/sdvx@asphyxia/templates/booth/load.pug new file mode 100644 index 0000000..3562737 --- /dev/null +++ b/sdvx@asphyxia/templates/booth/load.pug @@ -0,0 +1,23 @@ +game + name(__type="str") #{name} + code(__type="str") #{code} + gamecoin_packet(__type="u32") #{packets} + gamecoin_block(__type="u32") #{blocks} + exp_point(__type="u32") #{expPoint ? expPoint : 0} + m_user_cnt(__type="u32") #{mUserCnt ? mUserCnt : 0} + have_item(__type="bool" __count=512) #{Array(512).fill(1).join(" ")} + have_note(__type="bool" __count=512) #{Array(512).fill(1).join(" ")} + + last( + music_id=musicID, + music_type=musicType, + sort_type=sortType, + headphone=headphone, + hispeed=hiSpeed, + appeal_id=appeal, + frame0=boothFrame ? boothFrame[0] : 0, + frame1=boothFrame ? boothFrame[1] : 0, + frame2=boothFrame ? boothFrame[2] : 0, + frame3=boothFrame ? boothFrame[3] : 0, + frame4=boothFrame ? boothFrame[4] : 0, + ) diff --git a/sdvx@asphyxia/templates/load.pug b/sdvx@asphyxia/templates/load.pug index d6e9ea8..a3dd726 100644 --- a/sdvx@asphyxia/templates/load.pug +++ b/sdvx@asphyxia/templates/load.pug @@ -35,6 +35,8 @@ game packet_booster(__type="s32") 1 if version != 5 block_booster(__type="s32") 1 + blaster_pass_enable(__type="bool") 1 + blaster_pass_limit_date(__type="u64") #{currentTime} eaappli relation(__type="s8") 1 diff --git a/sdvx@asphyxia/utils.ts b/sdvx@asphyxia/utils.ts index 3fd5e88..93010c7 100644 --- a/sdvx@asphyxia/utils.ts +++ b/sdvx@asphyxia/utils.ts @@ -1,4 +1,5 @@ -import { Counter } from './models/counter'; +import {Counter} from './models/counter'; + export function IDToCode(id: number) { const padded = _.padStart(id.toString(), 8); return `${padded.slice(0, 4)}-${padded.slice(4)}`; @@ -12,3 +13,11 @@ export async function GetCounter(key: string) { ) ).docs[0].value; } + +export function getVersion(info: EamuseInfo) { + const dateCode = parseInt(info.model.split(":")[4]); + if (dateCode <= 2013052900) return 1; + if (info.method.startsWith('sv4')) return 4; + if (info.method.startsWith('sv5')) return 5; + return 0; +}