diff --git a/popn@asphyxia/README.md b/popn@asphyxia/README.md index bdbca7c..da82e43 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 +* Various 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..9f68537 100644 --- a/popn@asphyxia/handler/eclale.ts +++ b/popn@asphyxia/handler/eclale.ts @@ -11,6 +11,7 @@ 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); } const getInfoCommon = (req: EamuseInfo) => { @@ -130,16 +131,31 @@ 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, version) }); +}; + +const getScores = async (refid: string, version: 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,28 +164,24 @@ 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; }; const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { @@ -223,6 +235,7 @@ const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise */ 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; @@ -261,6 +274,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 +284,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]), @@ -424,6 +437,34 @@ const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise send.success(); }; +const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const no = parseInt($(data).attr()['no'], -1); + + 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 version: string = 'v23'; diff --git a/popn@asphyxia/handler/lapistoria.ts b/popn@asphyxia/handler/lapistoria.ts index 3828500..aa65e7d 100644 --- a/popn@asphyxia/handler/lapistoria.ts +++ b/popn@asphyxia/handler/lapistoria.ts @@ -8,6 +8,7 @@ export const setRoutes = () => { R.Route(`player22.read`, read); R.Route(`player22.write_music`, writeScore); R.Route(`player22.write`, write); + R.Route(`player22.friend`, friend); } /** @@ -110,6 +111,7 @@ const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise */ 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 +128,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 +137,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), @@ -372,6 +374,70 @@ const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise send.success(); }; +const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const no = parseInt($(data).attr()['no'], -1); + + 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/usaneko.ts b/popn@asphyxia/handler/usaneko.ts index 783ec17..08e4dfe 100644 --- a/popn@asphyxia/handler/usaneko.ts +++ b/popn@asphyxia/handler/usaneko.ts @@ -11,6 +11,7 @@ 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); } const getInfoCommon = (req: EamuseInfo) => { @@ -148,7 +149,7 @@ const readScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise< send.object({ music: await getScores(refid, version) }); }; -const getScores = async (refid: string, version: string) => { +const getScores = async (refid: string, version: string, forFriend: boolean = false) => { const scoresData = await utils.readScores(refid, version); const result = []; @@ -157,6 +158,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,26 +179,24 @@ 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; @@ -261,6 +273,7 @@ const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise */ 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; @@ -299,6 +312,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 +321,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]), @@ -381,7 +394,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)), @@ -595,6 +611,35 @@ const write = async (req: EamuseInfo, data: any, send: EamuseSend): Promise send.success(); }; +const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise => { + const refid = $(data).attr()['ref_id']; + const no = parseInt($(data).attr()['no'], -1); + 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..5d3d639 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,8 +36,29 @@ 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(refid, { 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)); @@ -44,7 +66,6 @@ export function register() { R.Route(`playerdata.set`, async (req, data, send) => getVersion(req).write(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