Merge pull request #18 from cracrayol/stable

Pop'n Music rivals support
This commit is contained in:
Freddie Wang 2021-04-21 01:03:10 +08:00 committed by GitHub
commit b58b69886b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 567 additions and 181 deletions

View File

@ -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

View File

@ -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<any>
};
/**
* Create a new profile and send it.
* Handler for new profile
*/
const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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<any> => {
const refid = $(data).str('ref_id');
@ -100,6 +104,9 @@ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any>
send.object(await getProfile(refid));
};
/**
* Handler fo buying goods with lumina
*/
const buy = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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<any> =
send.success();
};
/**
* Handler for getting the user scores
*/
const readScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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<any> => {
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<any> => {
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<any>
send.success();
};
/**
* Handler for sending rivals
*/
const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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';

View File

@ -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<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
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';

View File

@ -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<any> => {
const result: any = {
@ -38,7 +39,7 @@ const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<an
};
/**
* Create a new profile and send it.
* Handler for new profile
*/
const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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<any> => {
const refid = $(data).str('ref_id');
@ -59,6 +60,9 @@ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any>
send.object(await getProfile(refid));
};
/**
* Handler for saving the scores
*/
const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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<any> => {
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<any>
send.success();
};
/**
* Handler for sending rivals
*/
const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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';

View File

@ -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<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
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';

View File

@ -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<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
// No rivals support for Tune street :(
send.deny();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const version: string = 'v19';

View File

@ -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<any>
};
/**
* Create a new profile and send it.
* Handler for new profile
*/
const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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<any> => {
const refid = $(data).str('ref_id');
@ -112,6 +116,9 @@ const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any>
send.object(await getProfile(refid, getVersion(req)));
};
/**
* Handler fo buying goods with lumina
*/
const buy = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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<any> =
params.params.player_point = lumina - price;
await utils.writeParams(refid, version, params);
const achievements = <AchievementsUsaneko>await utils.readAchievements(refid, version, {...defaultAchievements, version});
const achievements = <AchievementsUsaneko>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<any> => {
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<any> => {
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 = <AchievementsUsaneko>await utils.readAchievements(refid, version, {...defaultAchievements, version});
const achievements = <AchievementsUsaneko>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<any> => {
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<any>
const version = getVersion(req);
const params = await utils.readParams(refid, version);
const achievements = <AchievementsUsaneko>await utils.readAchievements(refid, version, {...defaultAchievements, version});
const achievements = <AchievementsUsaneko>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<any>
send.success();
};
/**
* Handler for sending rivals
*/
const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
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 => {

View File

@ -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<Profile>(refid, { collection: 'profile' }, profile);
}
export const readRivals = async (refid: string): Promise<Rivals> => {
const rivals = await DB.FindOne<Rivals>(refid, { collection: 'rivals' });
return rivals || { collection: 'rivals', rivals: [] };
}
export const readParams = async (refid: string, version: string): Promise<Params> => {
const params = await DB.FindOne<Params>(refid, { collection: 'params', version });
return params || { collection: 'params', version, params: {} };

View File

@ -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<Rivals>(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<Rivals>(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();

View File

@ -28,6 +28,11 @@ export interface Params {
};
}
export interface Rivals {
collection: 'rivals',
rivals: string[]
}
export interface Scores {
collection: 'scores',
version: string,

View File

@ -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