plugins/popn@asphyxia/handler/usaneko.ts
2025-09-24 16:45:58 +02:00

1119 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AchievementsUsaneko } from "../models/achievements";
import { ExtraData, Phase } from "../models/common";
import * as utils from "./utils";
export const setRoutes = () => {
R.Route(`info24.common`, getInfo);
R.Route(`player24.new`, newPlayer);
R.Route(`player24.read`, read);
R.Route(`player24.start`, start);
R.Route(`player24.buy`, buy);
R.Route(`player24.read_score`, readScore);
R.Route(`player24.write_music`, writeScore);
R.Route(`player24.write`, write);
R.Route(`player24.friend`, friend);
}
/**
* Return current state of the game (phase, good prices, etc...)
*/
const getInfoCommon = (req: EamuseInfo) => {
const result: any = {
phase: [],
choco: [],
goods: [],
area: [],
};
// Phase
const version = getVersion(req);
const phaseData = getPhase(version);
for (const phase of phaseData) {
result.phase.push({
event_id: K.ITEM('s16', phase.id),
phase: K.ITEM('s16', phase.p),
});
}
// Choco
for (let i = 0; i < 5; ++i) {
result.choco.push({
choco_id: K.ITEM('s16', i),
param: K.ITEM('s32', -1),
});
}
// Goods
for (let i = 0; i < GAME_MAX_DECO_ID[version]; ++i) {
let price = 250;
if (i < 15) {
price = 30;
} else if (i < 30) {
price = 40;
} else if (i < 45) {
price = 60;
} else if (i < 60) {
price = 80;
} else if (i < 98) {
price = 200;
}
result.goods.push({
item_id: K.ITEM('s32', i + 1),
item_type: K.ITEM('s16', 3),
price: K.ITEM('s32', price),
goods_type: K.ITEM('s16', 0),
});
}
// Area
if(version == 'v24') {
for (let i = 0; i < 16; ++i) {
result.area.push({
area_id: K.ITEM('s16', i),
end_date: K.ITEM('u64', BigInt(0)),
medal_id: K.ITEM('s16', i),
is_limit: K.ITEM('bool', 0),
});
}
}
// TODO : Course ranking
// TODO : Most popular characters
// TODO : Most popular music
return result;
}
const getInfo = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
return send.object(getInfoCommon(req));
}
const start = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
const result = {
play_id: K.ITEM('s32', 1),
...getInfoCommon(req),
};
await send.object(result);
};
/**
* Handler for new profile
*/
const newPlayer = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
const refid = $(data).str('ref_id');
if (!refid) return send.deny();
const name = $(data).str('name');
send.object(await getProfile(refid, getVersion(req), name));
};
/**
* Handler for existing profile
*/
const read = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
const refid = $(data).str('ref_id');
if (!refid) return send.deny();
send.object(await getProfile(refid, getVersion(req)));
};
/**
* Handler fo buying goods with lumina
*/
const buy = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
const refid = $(data).str('ref_id');
if (!refid) return send.deny();
const type = $(data).number('type', -1);
const id = $(data).number('id', -1);
const param = $(data).number('param', 0);
const price = $(data).number('price', 0);
const lumina = $(data).number('lumina', 0);
if (type < 0 || id < 0) {
return send.deny();
}
if (lumina >= price) {
const version = getVersion(req);
const params = await utils.readParams(refid, version);
params.params.player_point = lumina - price;
await utils.writeParams(refid, version, params);
const achievements = <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);
if (!refid) return send.deny();
send.object({ music: await getScores(refid, version) });
};
/**
* Read the user scores and format them (profile/friend)
* @param refid ID of the user
* @param forFriend If true, format the output for friend request.
*/
const getScores = async (refid: string, version: string, forFriend: boolean = false) => {
const scoresData = await utils.readScores(refid, version);
const result = [];
for (const key in scoresData.scores) {
const keyData = key.split(':');
const score = scoresData.scores[key];
const music = parseInt(keyData[0], 10);
const sheet = parseInt(keyData[1], 10);
const clearType = {
100: 1,
200: 2,
300: 3,
400: 4,
500: 5,
600: 6,
700: 7,
800: 8,
900: 9,
1000: 10,
1100: 11,
}[score.clear_type] || 0;
if (!isOmni && (music > GAME_MAX_MUSIC_ID[version])) {
continue;
}
if ([0, 1, 2, 3].indexOf(sheet) == -1) {
continue;
}
if (forFriend) {
result.push(K.ATTR({
music_num: music.toString(),
sheet_num: sheet.toString(),
score: score.score.toString(),
cleartype: clearType.toString(),
clearrank: getRank(score.score).toString()
}));
} else {
result.push({
music_num: K.ITEM('s16', music),
sheet_num: K.ITEM('u8', sheet),
score: K.ITEM('s32', score.score),
clear_type: K.ITEM('u8', clearType),
clear_rank: K.ITEM('u8', getRank(score.score)),
cnt: K.ITEM('s16', score.cnt),
});
}
}
return result;
};
/**
* Return the rank based on the given score
*/
const getRank = (score: number): number => {
if (score < 50000) {
return 1
} else if (score < 62000) {
return 2
} else if (score < 72000) {
return 3
} else if (score < 82000) {
return 4
} else if (score < 90000) {
return 5
} else if (score < 95000) {
return 6
} else if (score < 98000) {
return 7
}
return 8
}
/**
* Handler for saving the scores
*/
const writeScore = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<any> => {
const version = getVersion(req);
const refid = $(data).str('ref_id');
if (!refid) return send.deny();
const music = $(data).number('music_num');
const sheet = $(data).number('sheet_num');
const clear_type = {
1: 100,
2: 200,
3: 300,
4: 400,
5: 500,
6: 600,
7: 700,
8: 800,
9: 900,
10: 1000,
11: 1100,
}[$(data).number('clear_type')];
const score = $(data).number('score');
const key = `${music}:${sheet}`;
const scoresData = await utils.readScores(refid, version, true);
if (!scoresData.scores[key]) {
scoresData.scores[key] = {
score,
cnt: 1,
clear_type
};
} else {
scoresData.scores[key] = {
score: Math.max(score, scoresData.scores[key].score),
cnt: scoresData.scores[key].cnt + 1,
clear_type: Math.max(clear_type, scoresData.scores[key].clear_type || 0)
};
}
utils.writeScores(refid, version, scoresData);
send.success();
};
/**
* Get/create the profile based on refid
* @param refid the profile refid
* @param name if defined, create/update the profile with the given name
*/
const getProfile = async (refid: string, version: string, name?: string) => {
const profile = await utils.readProfile(refid);
const rivals = await utils.readRivals(refid);
if (name && name.length > 0) {
profile.name = name;
await utils.writeProfile(refid, profile);
}
let myBest = Array(10).fill(-1);
const scores = await utils.readScores(refid, version, true);
if (Object.entries(scores.scores).length > 0) {
const playCount = new Map();
for (const key in scores.scores) {
const keyData = key.split(':');
const music = parseInt(keyData[0], 10);
playCount.set(music, (playCount.get(music) || 0) + scores.scores[key].cnt);
}
const sortedPlayCount = new Map([...playCount.entries()].sort((a, b) => b[1] - a[1]));
let i = 0;
for (const value of sortedPlayCount.keys()) {
if (i >= 10) {
break;
}
myBest[i] = value;
i++;
}
}
let player: any = {
result: K.ITEM('s8', 0),
account: {
name: K.ITEM('str', profile.name),
g_pm_id: K.ITEM('str', profile.friendId),
staff: K.ITEM('s8', 0),
item_type: K.ITEM('s16', 0),
item_id: K.ITEM('s16', 0),
is_conv: K.ITEM('s8', 0),
license_data: K.ARRAY('s16', Array(20).fill(-1)),
my_best: K.ARRAY('s16', myBest),
active_fr_num: K.ITEM('u8', rivals.rivals.length),
// TODO: replace with real data
total_play_cnt: K.ITEM('s16', 100),
today_play_cnt: K.ITEM('s16', 50),
consecutive_days: K.ITEM('s16', 365),
total_days: K.ITEM('s16', 366),
interval_day: K.ITEM('s16', 1),
latest_music: K.ARRAY('s16', [-1, -1, -1, -1, -1]),
},
netvs: {
record: K.ARRAY('s16', [0, 0, 0, 0, 0, 0]),
dialog: [
K.ITEM('str', 'dialog'),
K.ITEM('str', 'dialog'),
K.ITEM('str', 'dialog'),
K.ITEM('str', 'dialog'),
K.ITEM('str', 'dialog'),
K.ITEM('str', 'dialog'),
],
ojama_condition: K.ARRAY('s8', Array(74).fill(0)),
set_ojama: K.ARRAY('s8', [0, 0, 0]),
set_recommend: K.ARRAY('s8', [0, 0, 0]),
netvs_play_cnt: K.ITEM('u32', 0),
},
eaappli: {
relation: K.ITEM('s8', -1),
},
custom_cate: {
valid: K.ITEM('s8', 0),
lv_min: K.ITEM('s8', -1),
lv_max: K.ITEM('s8', -1),
medal_min: K.ITEM('s8', -1),
medal_max: K.ITEM('s8', -1),
friend_no: K.ITEM('s8', -1),
score_flg: K.ITEM('s8', -1),
},
// TODO: Navi data ??
navi_data: {
raisePoint: K.ARRAY('s32', [-1, -1, -1, -1, -1]),
navi_param: {
navi_id: K.ITEM('u16', 0),
friendship: K.ITEM('s32', 0),
},
},
music: await getScores(refid, version),
mission: [],
area: [],
course_data: [],
fes: [],
item: [],
chara_param: [],
stamp: [],
};
// Add version specific datas
let params = await utils.readParams(refid, version);
utils.addExtraData(player, params, getExtraData(version));
const achievements = <AchievementsUsaneko>await utils.readAchievements(refid, version, { ...defaultAchievements, version });
const profileCharas = achievements.charas || {};
for (const chara_id in profileCharas) {
player.chara_param.push({
chara_id: K.ITEM('u16', parseInt(chara_id, 10)),
friendship: K.ITEM('u16', profileCharas[chara_id]),
});
}
let profileStamps = achievements.stamps;
if (Object.entries(profileStamps).length == 0) {
profileStamps = { "0": 0 };
}
for (const stamp_id in profileStamps) {
player.stamp.push({
stamp_id: K.ITEM('s16', parseInt(stamp_id, 10)),
cnt: K.ITEM('s16', profileStamps[stamp_id]),
});
}
const profileAreas = achievements.areas || {};
for (const area_id in profileAreas) {
const area = profileAreas[area_id];
player.area.push({
area_id: K.ITEM('u32', parseInt(area_id, 10)),
chapter_index: K.ITEM('u8', area.chapter_index),
gauge_point: K.ITEM('u16', area.gauge_point),
is_cleared: K.ITEM('bool', area.is_cleared),
diary: K.ITEM('u32', area.diary),
});
}
const profileCourses = achievements.courses || {};
for (const course_id in profileCourses) {
const course = profileCourses[course_id];
player.course_data.push({
course_id: K.ITEM('s16', parseInt(course_id, 10)),
clear_type: K.ITEM('u8', course.clear_type),
clear_rank: K.ITEM('u8', course.clear_rank),
total_score: K.ITEM('s32', course.total_score),
update_count: K.ITEM('s32', course.update_count),
sheet_num: K.ITEM('u8', course.sheet_num),
});
}
const profileFes = achievements.fes || {};
for (const fes_id in profileFes) {
const fesElt = profileFes[fes_id];
player.fes.push({
fes_id: K.ITEM('u32', parseInt(fes_id, 10)),
chapter_index: K.ITEM('u8', fesElt.chapter_index),
gauge_point: K.ITEM('u16', fesElt.gauge_point),
is_cleared: K.ITEM('bool', fesElt.is_cleared),
});
}
const profileItems = achievements.items || {};
for (const key in profileItems) {
const keyData = key.split(':');
const type = parseInt(keyData[0], 10);
const id = parseInt(keyData[1], 10);
player.item.push({
type: K.ITEM('u8', type),
id: K.ITEM('u16', id),
param: K.ITEM('u16', profileItems[key]),
is_new: K.ITEM('bool', 0),
get_time: K.ITEM('u64', BigInt(0)),
});
}
if(U.GetConfig("enable_force_unlock")) {
for(let i = 1; i <= GAME_MAX_MUSIC_ID[version]; i++) {
player.item.push({
type: K.ITEM('u8', 0),
id: K.ITEM('u16', i),
param: K.ITEM('u16', 15),
is_new: K.ITEM('bool', 0),
get_time: K.ITEM('u64', BigInt(0)),
});
}
}
// Usaneko events
if (version == 'v24') {
const date = new Date();
const currentDate = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate();
if (!params.params.mission_date) {
params.params.mission_date = currentDate;
}
// Daily missions
const missions = achievements.missions || { 1: {}, 2: {}, 3: {} };
let maxId = parseInt(_.max(Object.keys(missions)), 10);
for (const id in missions) {
const mission = missions[id];
if (currentDate != params.params.mission_date) {
// New day => Check completion
if(mission.mission_comp == 1) {
// Mission completed => New mission
_.max(Object.keys(missions)) + 1
player.mission.push({
mission_id: K.ITEM('u32', ++maxId),
gauge_point: K.ITEM('u32', 0),
mission_comp: K.ITEM('u32', 0),
});
} else {
// Mission not completed => Reset counter
player.mission.push({
mission_id: K.ITEM('u32', parseInt(id, 10)),
gauge_point: K.ITEM('u32', 0),
mission_comp: K.ITEM('u32', 0),
});
}
} else {
player.mission.push({
mission_id: K.ITEM('u32', parseInt(id, 10)),
gauge_point: K.ITEM('u32', mission.gauge_point || 0),
mission_comp: K.ITEM('u32', mission.mission_comp || 0),
});
}
}
}
// Kaimei events
if (version == 'v26') {
// Kaimei! MN tanteisha
player.riddles_data = {
sp_riddles: [],
sh_riddles: []
}
player.riddles_data.sp_riddles = [];
const riddles = achievements.riddles || {};
let i = 0;
while (riddles[i] != undefined) {
const riddle = riddles[i];
player.riddles_data.sp_riddles.push({
kaimei_gauge: K.ITEM('u16', riddle.kaimei_gauge || 0),
is_cleared: K.ITEM('bool', riddle.is_cleared || false),
riddles_cleared: K.ITEM('bool', riddle.riddles_cleared || false),
select_count: K.ITEM('u8', riddle.select_count || 0),
other_count: K.ITEM('u32', riddle.other_count || 0),
});
i++;
};
// riddle id : 1 to 20
let randomRiddles = [];
for (let i = 0; i < 3; i++) {
let riddle = 0;
do {
riddle = Math.floor(Math.random() * 20) + 1;
} while (randomRiddles.indexOf(riddle) >= 0);
player.riddles_data.sh_riddles.push({ sh_riddles_id: K.ITEM('u32', riddle) });
}
}
// Unilab events
if (version == 'v27') {
const teams = achievements.team || [];
const batteries = achievements.battery || [];
player.event_p27.first_play = K.ITEM('bool', teams.length == 0);
player.event_p27.elem_first_play = K.ITEM('bool', batteries.length == 0);
player.event_p27.team = [];
for (const team of teams) {
player.event_p27.team.push({
team_id: K.ITEM('s16', team.team_id || 0),
ex_no: K.ITEM('s16', team.ex_no || 0),
point: K.ITEM('u32', team.point || 0),
is_cleared: K.ITEM('bool', team.is_cleared || false),
});
};
player.event_p27.battery = [];
for (const battery of batteries) {
player.event_p27.battery.push({
battery_id: K.ITEM('s16', battery.battery_id || 0),
energy: K.ITEM('u32', battery.energy || 0),
is_cleared: K.ITEM('bool', battery.is_cleared || false),
});
};
}
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();
const version = getVersion(req);
const params = await utils.readParams(refid, version);
const achievements = <AchievementsUsaneko>await utils.readAchievements(refid, version, { ...defaultAchievements, version });
utils.getExtraData(data, params, getExtraData(version, true));
// areas
let areas = _.get(data, 'area', []);
if (!achievements.areas) {
achievements.areas = {};
}
if (!_.isArray(areas)) {
areas = [areas];
}
for (const area of areas) {
const id = $(area).number('area_id');
const chapter_index = $(area).number('chapter_index');
const gauge_point = $(area).number('gauge_point');
const is_cleared = $(area).bool('is_cleared');
const diary = $(area).number('diary');
achievements.areas[id] = {
chapter_index,
gauge_point,
is_cleared,
diary,
};
}
// courses
let courses = _.get(data, 'course_data', []);
if (!achievements.courses) {
achievements.courses = {};
}
if (!_.isArray(courses)) {
courses = [courses];
}
for (const course of courses) {
const id = $(course).number('course_id');
const clear_type = $(course).number('clear_type');
const clear_rank = $(course).number('clear_rank');
const total_score = $(course).number('total_score');
const update_count = $(course).number('update_count');
const sheet_num = $(course).number('sheet_num');
achievements.courses[id] = {
clear_type,
clear_rank,
total_score,
update_count,
sheet_num,
};
}
// fes
let fes = _.get(data, 'fes', []);
if (!achievements.fes) {
achievements.fes = {};
}
if (!_.isArray(fes)) {
fes = [fes];
}
for (const fesElt of fes) {
const id = $(fesElt).number('fes_id');
const chapter_index = $(fesElt).number('chapter_index');
const gauge_point = $(fesElt).number('gauge_point');
const is_cleared = $(fesElt).bool('is_cleared');
achievements.fes[id] = {
chapter_index,
gauge_point,
is_cleared,
};
}
// items
let items = _.get(data, 'item', []);
if (!achievements.items) {
achievements.items = {};
}
if (!_.isArray(items)) {
items = [items];
}
for (const item of items) {
const type = $(item).number('type');
const id = $(item).number('id');
const param = $(item).number('param');
const key = `${type}:${id}`;
achievements.items[key] = param;
}
// charas
let charas = _.get(data, 'chara_param', []);
if (!achievements.charas) {
achievements.charas = {};
}
if (!_.isArray(charas)) {
charas = [charas];
}
for (const chara of charas) {
const id = $(chara).number('chara_id');
const param = $(chara).number('friendship');
achievements.charas[id] = param;
}
// stamps
let stamps = _.get(data, 'stamp', []);
if (!achievements.stamps) {
achievements.stamps = { '0': 0 };
}
if (!_.isArray(stamps)) {
stamps = [stamps];
}
for (const stamp of stamps) {
const id = $(stamp).number('stamp_id');
const cnt = $(stamp).number('cnt');
achievements.stamps[id] = cnt;
}
// usaneko (v24)
if (version == 'v24') {
// Daily missions
const date = new Date();
params.params.mission_date = date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate();
let missions = _.get(data, 'mission', []);
achievements.missions = {};
if (!_.isArray(missions)) {
missions = [missions];
}
for (const mission of missions) {
const id = $(mission).number('mission_id');
const gauge_point = $(mission).number('gauge_point');
const mission_comp = $(mission).number('mission_comp');
achievements.missions[id] = {
gauge_point,
mission_comp,
};
}
}
// riddles (v26)
if (version == 'v26') {
const playedRiddle = <number>params.params.sp_riddles_id;
let riddlesData = _.get(data, 'riddles_data', []);
let riddles = _.get(riddlesData, 'sp_riddles', []);
if (!achievements.riddles) {
achievements.riddles = {};
}
if (!_.isArray(riddles)) {
riddles = [riddles];
}
let i = 0;
for (const riddle of riddles) {
const kaimei_gauge = $(riddle).number('kaimei_gauge', 0);
const is_cleared = $(riddle).bool('is_cleared');
const riddles_cleared = $(riddle).bool('riddles_cleared');
let select_count = $(riddle).number('select_count', 0);
const other_count = $(riddle).number('other_count', 0);
if (riddles_cleared || select_count >= 3) {
// Show all hint if riddle cleared.
select_count = 3
} else if (playedRiddle == i) {
// Add a hint if riddle is select.
select_count++;
}
achievements.riddles[i] = {
kaimei_gauge,
is_cleared,
riddles_cleared,
select_count,
other_count,
};
i++;
}
}
// Unilab (v27)
if (version == 'v27') {
let eventData = _.get(data, 'event_p27', []);
let team = _.get(eventData, 'team', null);
if(_.isPlainObject(team)) {
if (_.isNil(achievements.team)) {
achievements.team = [];
}
const team_id = $(team).number('team_id');
const ex_no = $(team).number('ex_no');
const point = $(team).number('point');
const is_cleared = $(team).bool('is_cleared');
let savedTeam = _.find(achievements.team, {'team_id': team_id});
if(_.isUndefined(savedTeam)) {
achievements.team.push({
team_id,
ex_no,
point,
is_cleared
});
} else {
savedTeam.ex_no = ex_no;
savedTeam.point = point;
savedTeam.is_cleared = is_cleared;
}
}
let battery = _.get(eventData, 'battery', null);
if(_.isPlainObject(battery)) {
if (_.isNil(achievements.battery)) {
achievements.battery = [];
}
const battery_id = $(battery).number('battery_id');
const energy = $(battery).number('energy');
const is_cleared = $(battery).bool('is_cleared');
let savedBattery = _.find(achievements.battery, {'battery_id': battery_id});
if(_.isUndefined(savedBattery)) {
achievements.battery.push({
battery_id,
energy,
is_cleared
});
} else {
savedBattery.energy = energy;
savedBattery.is_cleared = is_cleared;
}
}
}
await utils.writeParams(refid, version, params);
await utils.writeAchievements(refid, version, achievements);
send.success();
};
/**
* Handler for sending rivals
*/
const friend = async (req: EamuseInfo, data: any, send: EamuseSend): Promise<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', profile.friendId),
name: K.ITEM('str', profile.name),
chara: K.ITEM('s16', params.params.chara || -1),
is_open: K.ITEM('s8', 1),
music: await getScores(rivals.rivals[no], version, true),
}
}
send.object(friend);
}
const getPhase = (version: String): Phase[] => {
let phase = [];
switch(version) {
case 'v27':
phase = PHASE['v27'];
break;
case 'v26':
phase = PHASE['v26'];
case 'v25':
phase = _.unionBy(phase, PHASE['v25'], 'id');
case 'v24':
phase = _.unionBy(phase, PHASE['v24'], 'id');
}
return _.sortBy(phase, 'id');
}
const getExtraData = (version: String, full: boolean = false): ExtraData => {
let extraData = EXTRA_DATA_COMMON;
if (full) {
extraData = _.merge(extraData, EXTRA_DATA_V27, EXTRA_DATA_V26);
} else {
switch(version) {
case 'v27':
extraData = _.merge(extraData, EXTRA_DATA_V27);
break;
case 'v26':
extraData = _.merge(extraData, EXTRA_DATA_V26);
break;
}
}
return extraData;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
let isOmni = false;
const getVersion = (req: EamuseInfo): string => {
if (req.model.indexOf('J:A:X') >= 0 || req.model.indexOf('J:B:X') >= 0 || req.model.indexOf('J:C:X') >= 0) {
isOmni = true;
}
const date: number = parseInt(req.model.match(/:(\d*)$/)[1]);
if (date >= 2022091300) {
return 'v27';
} else if (date >= 2021042600 && date < 2022091300) {
return 'v26';
} else if (date >= 2018101700 && date < 2021042600 ) {
return 'v25';
} else {
return 'v24';
}
}
const GAME_MAX_MUSIC_ID = {
v24: 1704,
v25: 1877,
v26: 2019,
v27: 2188
}
const GAME_MAX_DECO_ID = {
v24: 97,
v25: 133,
v26: 133,
v27: 81
}
const defaultAchievements: AchievementsUsaneko = {
collection: 'achievements',
version: null,
areas: {},
courses: {},
fes: {},
items: {},
charas: {},
stamps: {},
riddles: {},
missions: {},
team: [],
battery: []
}
const PHASE = {
v24: [
{ id: 0, p: 11 }, // Default song phase availability (0-11)
{ id: 1, p: 2 }, // Unknown event (0-2)
{ id: 2, p: 2 }, // Holiday Greeting (0-2)
{ id: 3, p: 4 }, // Unknown event (0-2)
{ id: 4, p: 1 }, // Unknown event (0-1)
{ id: 5, p: 0 }, // Enable Net Taisen (0-1)
{ id: 6, p: 1 }, // Enable NAVI-kun shunkyoku toujou, allows song 1608 to be unlocked (0-1)
{ id: 7, p: 1 }, // Unknown event (0-1)
{ id: 8, p: 2 }, // Unknown event (0-2)
{ id: 9, p: 2 }, // Daily Mission (0-2)
{ id: 10, p: 15 }, // NAVI-kun Song phase availability (0-15)
{ id: 11, p: 1 }, // Unknown event (0-1)
{ id: 12, p: 2 }, // Unknown event (0-2)
{ id: 13, p: 1 }, // Enable Pop'n Peace preview song (0-1)
],
v25: [
{ id: 0, p: 23 }, // Default song phase availability (0-23)
{ id: 1, p: 4 }, // Unknown event (0-4)
{ id: 10, p: 30 }, // NAVI-kun Song phase availability (0-30)
{ id: 14, p: 39 }, // Stamp Card Rally (0-39)
{ id: 15, p: 2 }, // Unknown event (0-2)
{ id: 16, p: 3 }, // Unknown event (0-3)
{ id: 17, p: 8 }, // Unknown event (0-8)
{ id: 18, p: 1 }, // FLOOR INFECTION event (0-1)
{ id: 19, p: 1 }, // Pop'n music × NOSTALGIA kyouenkai (0-1)
{ id: 20, p: 13 }, // Event archive (0-13)
{ id: 21, p: 20 }, // Pop'n event archive (0-20)
{ id: 22, p: 2 }, // バンめし♪ ふるさとグランプリ (0-2)
{ id: 23, p: 1 }, // いちかのBEMANI投票選抜戦2019 (0-1)
{ id: 24, p: 1 }, // ダンキラ!!! × pop'n music (0-1)
],
v26: [
{ id: 0, p: 30 }, // Music phase (0: No unlock, 1-30: steps)
{ id: 25, p: 62 }, // M&N event (0: disable, 62: all characters)
{ id: 26, p: 3 }, // Unknown event (0-3)
{ id: 27, p: 2 }, // peace soundtrack hatsubai kinen SP (0: not started, 1: enabled, 2: ended)
{ id: 28, p: 2 }, // MZD no kimagure tanteisha joshu (0: not started, 1: enabled, 2: ended)
{ id: 29, p: 5 }, // Shutchou! pop'n quest Lively (0: not started, 1-4: step enabled, 5: ended)
{ id: 30, p: 6 }, // Shutchou! pop'n quest Lively II (0: not started, 1-5: step enabled, 6: ended)
],
v27: [
{ id: 0, p: 6 }, // Music phase (0: No unlock, 1-6: steps)
{ id: 1, p: 6 }, // Shutchou! pop'n quest Lively II (0: not started, 1-5: steps, 6: ended)
{ id: 2, p: 4 }, // KAC 2023 (0/2/4: disabled, 1: Caldwell 99, 3: Hexer / mathematical good-bye)
{ id: 3, p: 0 }, // Net Taisen (0: diabled, 1: enabled, 2: enabled + local)
{ id: 4, p: 7 }, // Unknown event (0-7)
{ id: 5, p: 48 }, // Narunaru♪ UniLab jikkenshitsu! event (0: not started, 1-47: steps, 48: ended)
{ id: 6, p: 2 }, // Super Unilab BOOST! (0: disabled, 1: enabled, 2: ended)
{ id: 7, p: 6 }, // Unknown event (0-6)
{ id: 8, p: 2 }, // Unknown event (0-2)
{ id: 9, p: 44 }, // Kakusei no Elem event (0: not started, 1-44: steps)
{ id: 10, p: 1 }, // Awakening Elem (0: disabled, 1: enabled)
{ id: 11, p: 2 }, // CanCan's Super Awakening Boost (0: disabled, 1: enabled, 2: ended)
{ id: 12, p: 2 }, // Unknown event (0-2)
{ id: 13, p: 2 }, // Unknown event (0-2)
]
}
const EXTRA_DATA_COMMON: ExtraData = {
play_id: { type: 's32', path: 'account', default: 0 },
start_type: { type: 's8', path: 'account', default: 0 },
tutorial: { type: 's16', path: 'account', default: -1 },
area_id: { type: 's16', path: 'account', default: 51 },
read_news: { type: 's16', path: 'account', default: 0 },
nice: { type: 's16', path: 'account', default: Array(30).fill(-1), isArray: true },
favorite_chara: { type: 's16', path: 'account', default: Array(20).fill(-1), isArray: true },
special_area: { type: 's16', path: 'account', default: Array(8).fill(-1), isArray: true },
chocolate_charalist: { type: 's16', path: 'account', default: Array(5).fill(-1), isArray: true },
chocolate_sp_chara: { type: 's32', path: 'account', default: 0 },
chocolate_pass_cnt: { type: 's32', path: 'account', default: 0 },
chocolate_hon_cnt: { type: 's32', path: 'account', default: 0 },
chocolate_giri_cnt: { type: 's32', path: 'account', default: 0 },
chocolate_kokyu_cnt: { type: 's32', path: 'account', default: 0 },
teacher_setting: { type: 's16', path: 'account', default: Array(10).fill(-1), isArray: true },
welcom_pack: { type: 'bool', path: 'account', default: 0 },
use_navi: { type: 's16', path: 'account', default: 0 },
ranking_node: { type: 's32', path: 'account', default: 0 },
chara_ranking_kind_id: { type: 's32', path: 'account', default: 0 },
navi_evolution_flg: { type: 's8', path: 'account', default: 0 },
ranking_news_last_no: { type: 's32', path: 'account', default: 0 },
power_point: { type: 's32', path: 'account', default: 0 },
player_point: { type: 's32', path: 'account', default: 300 },
power_point_list: { type: 's32', path: 'account', default: [0], isArray: true },
mode: { type: 'u8', path: 'config', default: 0 },
chara: { type: 's16', path: 'config', default: 0 },
music: { type: 's16', path: 'config', default: 0 },
sheet: { type: 'u8', path: 'config', default: 0 },
category: { type: 's8', path: 'config', default: 0 },
sub_category: { type: 's8', path: 'config', default: 0 },
chara_category: { type: 's8', path: 'config', default: 0 },
ms_banner_disp: { type: 's8', path: 'config', default: 0 },
ms_down_info: { type: 's8', path: 'config', default: 0 },
ms_side_info: { type: 's8', path: 'config', default: 0 },
ms_raise_type: { type: 's8', path: 'config', default: 0 },
ms_rnd_type: { type: 's8', path: 'config', default: 0 },
banner_sort: { type: 's8', path: 'config', default: 0 },
course_id: { type: 's16', path: 'config', default: 0 },
course_folder: { type: 's8', path: 'config', default: 0 },
hispeed: { type: 's16', path: 'option', default: 10 },
popkun: { type: 'u8', path: 'option', default: 0 },
hidden: { type: 'bool', path: 'option', default: 0 },
hidden_rate: { type: 's16', path: 'option', default: -1 },
sudden: { type: 'bool', path: 'option', default: 0 },
sudden_rate: { type: 's16', path: 'option', default: -1 },
randmir: { type: 's8', path: 'option', default: 0 },
gauge_type: { type: 's8', path: 'option', default: 0 },
ojama_0: { type: 'u8', path: 'option', default: 0 },
ojama_1: { type: 'u8', path: 'option', default: 0 },
forever_0: { type: 'bool', path: 'option', default: 0 },
forever_1: { type: 'bool', path: 'option', default: 0 },
full_setting: { type: 'bool', path: 'option', default: 0 },
guide_se: { type: 's8', path: 'option', default: 0 },
judge: { type: 'u8', path: 'option', default: 0 },
ep: { type: 'u16', path: 'info', default: 0 },
effect_left: { type: 'u16', path: 'customize', default: 0 },
effect_center: { type: 'u16', path: 'customize', default: 0 },
effect_right: { type: 'u16', path: 'customize', default: 0 },
hukidashi: { type: 'u16', path: 'customize', default: 0 },
comment_1: { type: 'u16', path: 'customize', default: 0 },
comment_2: { type: 'u16', path: 'customize', default: 0 },
}
const EXTRA_DATA_V26: ExtraData = {
card_again_count: { type: 's16', path: 'account', default: 0 },
sp_riddles_id: { type: 's16', path: 'account', default: -1 },
point: { type: 'u32', path: 'event2021', default: 0 }, // for peace soundtrack hatsubai kinen SP
step: { type: 'u8', path: 'event2021', default: 0 }, // for Shutchou! pop'n quest Lively
quest_point: { type: 'u32', path: 'event2021', default: Array(8).fill(0), isArray: true }, // for Shutchou! pop'n quest Lively
step_nos: { type: 'u8', path: 'event2021', default: 0 }, // for Shutchou! pop'n quest Lively II
quest_point_nos: { type: 'u32', path: 'event2021', default: Array(13).fill(0), isArray: true }, // for Shutchou! pop'n quest Lively II
}
const EXTRA_DATA_V27: ExtraData = {
lift: { type: 'bool', path: 'option', default: 0 },
lift_rate: { type: 's16', path: 'option', default: 0 },
team_id: { type: 's16', path: 'event_p27', default: 0 },
select_battery_id: { type: 's16', path: 'event_p27', default: 1 },
today_first_play: { type: 'bool', path: 'event_p27', default: 1 },
}