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 = []; const maxMusicId = GAME_MAX_MUSIC_ID[isOmni ? 'omni' : version]; for (const key in scoresData.scores) { const keyData = key.split(':'); const score = scoresData.scores[key]; const music = parseInt(keyData[0], 10); const sheet = parseInt(keyData[1], 10); 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] || 0; if (music > maxMusicId) { 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', profile.friendId), 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', profile.friendId), 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); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// let isOmni= false; const getVersion = (req: EamuseInfo): string => { if(req.model.indexOf('J:A:X') >= 0) { isOmni = true; } 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, omni: 3155 } 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 }, }