diff --git a/popn@asphyxia/README.md b/popn@asphyxia/README.md index bdbca7c..68c2547 100644 --- a/popn@asphyxia/README.md +++ b/popn@asphyxia/README.md @@ -1,6 +1,6 @@ # Pop'n Music -Plugin Version: **v2.0.0** +Plugin Version: **v2.1.0** ## Supported Versions - pop'n music 19 Tune Street @@ -15,6 +15,10 @@ 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 diff --git a/popn@asphyxia/handler/eclale.ts b/popn@asphyxia/handler/eclale.ts index 6d071eb..7540fed 100644 --- a/popn@asphyxia/handler/eclale.ts +++ b/popn@asphyxia/handler/eclale.ts @@ -11,8 +11,12 @@ export const setRoutes = () => { 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: [], @@ -79,7 +83,7 @@ const start = async (req: EamuseInfo, data: any, send: EamuseSend): Promise }; /** - * Create a new profile and send it. + * Handler for new profile */ const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -91,7 +95,7 @@ const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise< }; /** - * Read a profile and send it. + * Handler for existing profile */ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -100,6 +104,9 @@ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise 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(); @@ -126,20 +133,43 @@ const buy = async (req: EamuseInfo, data: any, send: EamuseSend): Promise = 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: any = { - music: [], - }; + 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; @@ -148,30 +178,29 @@ const readScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise< continue; } - result.music.push({ - music_num: K.ITEM('s16', music), - sheet_num: K.ITEM('u8', sheet), - score: K.ITEM('s32', score.score), - clear_type: K.ITEM('u8', { - 100: 1, - 200: 2, - 300: 3, - 400: 4, - 500: 5, - 600: 6, - 700: 7, - 800: 8, - 900: 9, - 1000: 10, - 1100: 11, - }[score.clear_type]), - cnt: K.ITEM('s16', score.cnt), - }); + 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), + }); + } } - - send.object(result); + 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(); @@ -219,10 +248,10 @@ const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise * Get/create the profile based on refid * @param refid the profile refid * @param name if defined, create/update the profile with the given name - * @returns */ const getProfile = async (refid: string, name?: string) => { const profile = await utils.readProfile(refid); + const rivals = await utils.readRivals(refid); if (name && name.length > 0) { profile.name = name; @@ -231,9 +260,9 @@ const getProfile = async (refid: string, name?: string) => { let myBest = Array(10).fill(-1); const scores = await utils.readScores(refid, version, true); - if(Object.entries(scores.scores).length > 0) { + if (Object.entries(scores.scores).length > 0) { const playCount = new Map(); - for(const key in scores.scores) { + 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); @@ -242,7 +271,7 @@ const getProfile = async (refid: string, name?: string) => { 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) { + if (i >= 10) { break; } myBest[i] = value; @@ -261,6 +290,7 @@ const getProfile = async (refid: string, name?: string) => { 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), @@ -270,7 +300,6 @@ const getProfile = async (refid: string, name?: string) => { interval_day: K.ITEM('s16', 1), my_best: K.ARRAY('s16', myBest), latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), - active_fr_num: K.ITEM('u8', 0), }, netvs: { record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), @@ -347,6 +376,9 @@ const getProfile = async (refid: string, name?: string) => { 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(); @@ -424,6 +456,37 @@ const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise 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'; diff --git a/popn@asphyxia/handler/fantasia.ts b/popn@asphyxia/handler/fantasia.ts index c6a5a36..15c1a8e 100644 --- a/popn@asphyxia/handler/fantasia.ts +++ b/popn@asphyxia/handler/fantasia.ts @@ -2,7 +2,7 @@ import { ExtraData } from "../models/common"; import * as utils from "./utils"; /** - * Return the current phases of the game. + * Handler for getting the current state of the game. */ export const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const result = { @@ -31,7 +31,7 @@ export const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Pro }; /** - * Create a new profile and send it. + * Handler for new profile */ export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -43,7 +43,7 @@ export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): P }; /** - * Read a profile and send it. + * Handler for existing profile */ export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -59,6 +59,7 @@ export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promis */ 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; @@ -66,53 +67,11 @@ export const getProfile = async (refid: string, name?: string) => { } // Get Score - let hiscore_array = Array(Math.floor((((GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8)).fill(0); - let clear_medal = Array(GAME_MAX_MUSIC_ID).fill(0); + const scores = await getScores(refid); let clear_medal_sub = 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); - } - let myBest = Array(20).fill(-1); - const sortedPlayCount = new Map([...playCount.entries()].sort((a, b) => b[1] - a[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) { @@ -129,15 +88,15 @@ export const getProfile = async (refid: string, name?: string) => { staff: K.ITEM('s8', 0), is_conv: K.ITEM('s8', -1), my_best: K.ARRAY('s16', myBest), - clear_medal: K.ARRAY('u16', clear_medal), + 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]), - active_fr_num: K.ITEM('u8', 0), }, player_card: { // TODO: replace with real data @@ -162,7 +121,7 @@ export const getProfile = async (refid: string, name?: string) => { set_recommend: K.ARRAY('s8', [0, 0, 0]), jewelry: K.ARRAY('s8', Array(15).fill(0)), }, - hiscore: K.ITEM('bin', Buffer.from(hiscore_array)) + hiscore: K.ITEM('bin', Buffer.from(scores.hiscore_array)) }; // Add version specific datas @@ -175,7 +134,7 @@ export const getProfile = async (refid: string, name?: string) => { } /** - * Unformat and write the end game data into DB + * Handler for saving profile ans scores */ export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).attr()['ref_id']; @@ -238,6 +197,91 @@ export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promi 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'; diff --git a/popn@asphyxia/handler/lapistoria.ts b/popn@asphyxia/handler/lapistoria.ts index 3828500..659a298 100644 --- a/popn@asphyxia/handler/lapistoria.ts +++ b/popn@asphyxia/handler/lapistoria.ts @@ -8,10 +8,11 @@ export const setRoutes = () => { R.Route(`player22.read`, read); R.Route(`player22.write_music`, writeScore); R.Route(`player22.write`, write); + R.Route(`player22.friend`, friend); } /** - * Return info22.common informations (phase, etc...) + * Handler for getting current state of the game. */ const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const result: any = { @@ -38,7 +39,7 @@ const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -50,7 +51,7 @@ const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise< }; /** - * Read a profile and send it. + * Handler for existing profile */ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -59,6 +60,9 @@ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise 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(); @@ -106,10 +110,10 @@ const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise * Get/create the profile based on refid * @param refid the profile refid * @param name if defined, create/update the profile with the given name - * @returns */ const getProfile = async (refid: string, name?: string) => { const profile = await utils.readProfile(refid); + const rivals = await utils.readRivals(refid); if (name && name.length > 0) { profile.name = name; @@ -126,6 +130,7 @@ const getProfile = async (refid: string, name?: string) => { 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), @@ -134,7 +139,6 @@ const getProfile = async (refid: string, name?: string) => { total_days: K.ITEM('s16', 366), interval_day: K.ITEM('s16', 1), latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), - active_fr_num: K.ITEM('u8', 0), }, netvs: { rank_point: K.ITEM('s32', 0), @@ -278,6 +282,9 @@ const getProfile = async (refid: string, name?: string) => { 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(); @@ -372,6 +379,73 @@ const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise 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'; diff --git a/popn@asphyxia/handler/sunny.ts b/popn@asphyxia/handler/sunny.ts index f73c50c..7dff458 100644 --- a/popn@asphyxia/handler/sunny.ts +++ b/popn@asphyxia/handler/sunny.ts @@ -2,7 +2,7 @@ import { ExtraData } from "../models/common"; import * as utils from "./utils"; /** - * Return the current phases of the game. + * 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 = { @@ -19,15 +19,15 @@ export const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Pro l_matching_sec: K.ITEM('s32', 60), is_check_cpu: K.ITEM('s32', 0), week_no: K.ITEM('s32', 0), - sel_ranking: K.ARRAY('s16', [-1, -1, -1, -1, -1]), - up_ranking: K.ARRAY('s16', [-1, -1, -1, -1, -1]), + sel_ranking: K.ARRAY('s16', Array(10).fill(-1)), + up_ranking: K.ARRAY('s16', Array(10).fill(-1)), }; return send.object(result); }; /** - * Create a new profile and send it. + * Handler for new profile */ export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -39,7 +39,7 @@ export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): P }; /** - * Read a profile and send it. + * Handler for existing profile */ export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -55,6 +55,7 @@ export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promis */ 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; @@ -62,53 +63,11 @@ export const getProfile = async (refid: string, name?: string) => { } // Get Score - let hiscore_array = Array(Math.floor((((GAME_MAX_MUSIC_ID * 4) * 17) + 7) / 8)).fill(0); - let clear_medal = Array(GAME_MAX_MUSIC_ID).fill(0); + const scores = await getScores(refid); let clear_medal_sub = 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); - } - let myBest = Array(20).fill(-1); - const sortedPlayCount = new Map([...playCount.entries()].sort((a, b) => b[1] - a[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) { @@ -126,15 +85,15 @@ export const getProfile = async (refid: string, name?: string) => { is_conv: K.ITEM('s8', -1), collabo: K.ITEM('u8', 255), my_best: K.ARRAY('s16', myBest), - clear_medal: K.ARRAY('u16', clear_medal), + 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]), - active_fr_num: K.ITEM('u8', 0), }, netvs: { rank_point: K.ITEM('s32', 0), @@ -154,7 +113,7 @@ export const getProfile = async (refid: string, name?: string) => { set_recommend: K.ARRAY('s8', [0, 0, 0]), netvs_play_cnt: K.ITEM('u8', 0), }, - hiscore: K.ITEM('bin', Buffer.from(hiscore_array)), + hiscore: K.ITEM('bin', Buffer.from(scores.hiscore_array)), gakuen_data: { music_list: K.ITEM('s32', -1), }, @@ -215,7 +174,7 @@ export const getProfile = async (refid: string, name?: string) => { } /** - * Unformat and write the end game data into DB + * Handler for saving profile and scores */ export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).attr()['ref_id']; @@ -273,6 +232,97 @@ export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promi 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'; diff --git a/popn@asphyxia/handler/tunestreet.ts b/popn@asphyxia/handler/tunestreet.ts index efca0fd..f63c937 100644 --- a/popn@asphyxia/handler/tunestreet.ts +++ b/popn@asphyxia/handler/tunestreet.ts @@ -1,5 +1,8 @@ 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" }); @@ -7,7 +10,7 @@ export const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Pro }; /** - * Create a new profile and send it. + * Handler for new profile */ export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).attr()['ref_id']; @@ -19,7 +22,7 @@ export const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): P }; /** - * Read a profile and send it. + * Handler for existing profile */ export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).attr()['ref_id']; @@ -29,10 +32,9 @@ export const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promis }; /** - * Get/create the profile based on 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 - * @returns */ export const getProfile = async (refid: string, name?: string) => { const profile = await utils.readProfile(refid); @@ -185,6 +187,9 @@ const __format_flags_for_score = (sheet: number, clear_type: number) => { 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(); @@ -288,6 +293,11 @@ export const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promi 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'; diff --git a/popn@asphyxia/handler/usaneko.ts b/popn@asphyxia/handler/usaneko.ts index 783ec17..95a5a4e 100644 --- a/popn@asphyxia/handler/usaneko.ts +++ b/popn@asphyxia/handler/usaneko.ts @@ -11,8 +11,12 @@ export const setRoutes = () => { 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: [], @@ -91,7 +95,7 @@ const start = async (req: EamuseInfo, data: any, send: EamuseSend): Promise }; /** - * Create a new profile and send it. + * Handler for new profile */ const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -103,7 +107,7 @@ const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise< }; /** - * Read a profile and send it. + * Handler for existing profile */ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { const refid = $(data).str('ref_id'); @@ -112,6 +116,9 @@ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise 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(); @@ -133,13 +140,16 @@ const buy = async (req: EamuseInfo, data: any, send: EamuseSend): Promise = params.params.player_point = lumina - price; await utils.writeParams(refid, version, params); - const achievements = await utils.readAchievements(refid, version, {...defaultAchievements, version}); + 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); @@ -148,7 +158,12 @@ const readScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise< send.object({ music: await getScores(refid, version) }); }; -const getScores = async (refid: string, version: string) => { +/** + * 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 = []; @@ -157,6 +172,19 @@ const getScores = async (refid: string, version: string) => { 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; @@ -165,31 +193,32 @@ const getScores = async (refid: string, version: string) => { continue; } - result.push({ - music_num: K.ITEM('s16', music), - sheet_num: K.ITEM('u8', sheet), - score: K.ITEM('s32', score.score), - clear_type: K.ITEM('u8', { - 100: 1, - 200: 2, - 300: 3, - 400: 4, - 500: 5, - 600: 6, - 700: 7, - 800: 8, - 900: 9, - 1000: 10, - 1100: 11, - }[score.clear_type]), - clear_rank: K.ITEM('u8', getRank(score.score)), - cnt: K.ITEM('s16', score.cnt), - }); + 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 @@ -209,6 +238,9 @@ const getRank = (score: number): number => { 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'); @@ -257,10 +289,10 @@ const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise * Get/create the profile based on refid * @param refid the profile refid * @param name if defined, create/update the profile with the given name - * @returns */ const getProfile = async (refid: string, version: string, name?: string) => { const profile = await utils.readProfile(refid); + const rivals = await utils.readRivals(refid); if (name && name.length > 0) { profile.name = name; @@ -269,9 +301,9 @@ const getProfile = async (refid: string, version: string, name?: string) => { let myBest = Array(10).fill(-1); const scores = await utils.readScores(refid, version, true); - if(Object.entries(scores.scores).length > 0) { + if (Object.entries(scores.scores).length > 0) { const playCount = new Map(); - for(const key in scores.scores) { + 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); @@ -280,7 +312,7 @@ const getProfile = async (refid: string, version: string, name?: string) => { 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) { + if (i >= 10) { break; } myBest[i] = value; @@ -299,6 +331,7 @@ const getProfile = async (refid: string, version: string, name?: string) => { 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), @@ -307,7 +340,6 @@ const getProfile = async (refid: string, version: string, name?: string) => { total_days: K.ITEM('s16', 366), interval_day: K.ITEM('s16', 1), latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]), - active_fr_num: K.ITEM('u8', 0), }, netvs: { record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]), @@ -371,7 +403,7 @@ const getProfile = async (refid: string, version: string, name?: string) => { stamp: [], }; - const achievements = await utils.readAchievements(refid, version, {...defaultAchievements, version}); + const achievements = await utils.readAchievements(refid, version, { ...defaultAchievements, version }); const profileCharas = achievements.charas || {}; for (const chara_id in profileCharas) { @@ -381,7 +413,10 @@ const getProfile = async (refid: string, version: string, name?: string) => { }); } - const profileStamps = achievements.stamps || { '0': 0 }; + 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)), @@ -449,6 +484,9 @@ const getProfile = async (refid: string, version: string, name?: string) => { 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(); @@ -456,7 +494,7 @@ const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise const version = getVersion(req); const params = await utils.readParams(refid, version); - const achievements = await utils.readAchievements(refid, version, {...defaultAchievements, version}); + const achievements = await utils.readAchievements(refid, version, { ...defaultAchievements, version }); utils.getExtraData(data, params, EXTRA_DATA); @@ -595,6 +633,38 @@ const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise 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 => { diff --git a/popn@asphyxia/handler/utils.ts b/popn@asphyxia/handler/utils.ts index a742bdd..53c7654 100644 --- a/popn@asphyxia/handler/utils.ts +++ b/popn@asphyxia/handler/utils.ts @@ -1,5 +1,5 @@ import { Achievements } from "../models/achievements"; -import { Profile, Scores, ExtraData, Params } from "../models/common"; +import { Profile, Scores, ExtraData, Params, Rivals } from "../models/common"; const CURRENT_DATA_VERSION = 2; @@ -68,6 +68,11 @@ 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: {} }; diff --git a/popn@asphyxia/index.ts b/popn@asphyxia/index.ts index 12a6877..cb63096 100644 --- a/popn@asphyxia/index.ts +++ b/popn@asphyxia/index.ts @@ -5,6 +5,7 @@ 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) { @@ -24,7 +25,7 @@ export function register() { R.Config("enable_score_sharing", { name: "Score sharing", - desc: "Enable sharing scores between versions", + desc: "Enable sharing scores between versions. This also affect rivals scores.", type: "boolean", default: true, }); @@ -35,16 +36,37 @@ export function register() { await DB.Update(data.refid, { collection: 'profile' }, { $set: { name: data.name } }); }); - // Route management for PnM <= 21 + // 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(); diff --git a/popn@asphyxia/models/common.ts b/popn@asphyxia/models/common.ts index 7bc4d54..b6160f1 100644 --- a/popn@asphyxia/models/common.ts +++ b/popn@asphyxia/models/common.ts @@ -28,6 +28,11 @@ export interface Params { }; } +export interface Rivals { + collection: 'rivals', + rivals: string[] +} + export interface Scores { collection: 'scores', version: string, diff --git a/popn@asphyxia/webui/profile_page.pug b/popn@asphyxia/webui/profile_page.pug index 4190081..3cecda4 100644 --- a/popn@asphyxia/webui/profile_page.pug +++ b/popn@asphyxia/webui/profile_page.pug @@ -1,5 +1,6 @@ //DATA// profile: DB.FindOne(refid, { collection: 'profile' }) + rivals: DB.FindOne(refid, { collection: 'rivals' }) div div.notification.is-success.is-hidden#import-success @@ -23,6 +24,44 @@ div 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