Merge pull request #42 from thomeval/feature/00_betterMDB

Feature/00 better mdb
This commit is contained in:
Freddie Wang 2022-05-04 13:03:45 +08:00 committed by GitHub
commit 1a2e955dd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 314 additions and 157 deletions

View File

@ -1,6 +1,6 @@
GITADORA Plugin for Asphyxia-Core
=================================
![Version: v1.2.1](https://img.shields.io/badge/version-v1.2.1-blue)
![Version: v1.2.2](https://img.shields.io/badge/version-v1.2.2-blue)
This plugin is based on converted from public-exported Asphyxia's Routes.
@ -26,10 +26,18 @@ Known Issues
* ~Information dialog keep showing as plugin doesn't store item data currently.~ (Fixed as of version 1.2.1)
* Special Premium Encore on Nextage
- Bandage solution is implemented. Try it.
* Friends and Rivals are unimplemented.
Release Notes
=============
v1.2.2
----------------
* Major improvements to the MDB (song data) loader. MDB files can now be in .json, .xml or .b64 format. This applies to both the per-version defaults and custom MDBs. To use a custom MDB, enable it in the web UI, and place a 'custom.xml', 'custom.json' or 'custom.b64' file in the data/mdb subfolder.
* Added several player profile stats to the web UI.
* MDB loader now logs the number of loaded songs available to GF and DM when in dev mode.
* MDB: Fixed "is_secret" field being ignored (always set to false)
v1.2.1
----------------
* Secret Music (unlocked songs) are now saved and loaded correctly. Partially fixes Github issue #34. Note that all songs are already marked as unlocked by the server - there is no need to unlock them manually. If you would like to lock them, consider using a custom MDB.

View File

@ -6,4 +6,5 @@ ex.json
mt.json
nt.json
hv.json
custom.xml
custom.xml
custom.json

View File

@ -1,18 +1,6 @@
import Logger from "../../utils/logger";
import { CommonMusicData } from "../../models/commonmusicdata";
export interface CommonMusicDataField {
id: KITEM<"s32">;
cont_gf: KITEM<"bool">;
cont_dm: KITEM<"bool">;
is_secret: KITEM<"bool">;
is_hot: KITEM<"bool">;
data_ver: KITEM<"s32">;
diff: KARRAY<"u16">;
}
export interface CommonMusicData {
music: CommonMusicDataField[]
}
export enum DATAVersion {
HIGHVOLTAGE = "hv",
@ -21,6 +9,9 @@ export enum DATAVersion {
MATTIX = "mt"
}
const allowedFormats = ['.json', '.xml', '.b64']
const mdbFolder = "data/mdb/"
type processRawDataHandler = (path: string) => Promise<CommonMusicData>
const logger = new Logger("mdb")
@ -32,30 +23,43 @@ export async function readXML(path: string) {
return json
}
export async function readJSON(path: string) {
logger.debugInfo(`Loading MDB data from ${path}.`)
const str = await IO.ReadFile(path, 'utf-8');
const json = JSON.parse(str)
return json
}
export async function readMDBFile(path: string, processHandler?: processRawDataHandler): Promise<CommonMusicData> {
export async function readJSONOrXML(jsonPath: string, xmlPath: string, processHandler: processRawDataHandler): Promise<CommonMusicData> {
if (!IO.Exists(jsonPath)) {
logger.debugInfo(`Loading MDB data from ${xmlPath}.`)
const data = await processHandler(xmlPath)
await IO.WriteFile(jsonPath, JSON.stringify(data))
return data
} else {
logger.debugInfo(`Loading MDB data from ${jsonPath}.`)
const json = JSON.parse(await IO.ReadFile(jsonPath, 'utf-8'))
return json
if (!IO.Exists(path)) {
throw "Unable to find MDB file at " + path
}
}
export async function readB64JSON(b64path: string) {
logger.debugInfo(`Loading MDB data from ${b64path}.`)
const buff = await IO.ReadFile(b64path, 'utf-8');
return JSON.parse(Buffer.from(buff, 'base64').toString('utf-8'));
logger.debugInfo(`Loading MDB data from ${path}.`)
let result : CommonMusicData;
const fileType = path.substring(path.lastIndexOf('.')).toLowerCase()
switch (fileType) {
case '.json':
const str = await IO.ReadFile(path, 'utf-8');
result = JSON.parse(str)
break;
case '.xml':
processHandler ?? defaultProcessRawXmlData
result = await processHandler(path)
// Uncomment to save the loaded XML file as JSON.
// await IO.WriteFile(path.replace(".xml", ".json"), JSON.stringify(data))
break;
case '.b64':
const buff = await IO.ReadFile(path, 'utf-8');
const json = Buffer.from(buff, 'base64').toString('utf-8')
// Uncomment to save the decoded base64 file as JSON.
// await IO.WriteFile(path.replace(".b64",".json"), json)
result = JSON.parse(json)
break;
default:
throw `Invalid MDB file type: ${fileType}. Only .json, .xml, .b64 are supported.`
}
let gfCount = result.music.filter((e) => e.cont_gf["@content"][0]).length
let dmCount = result.music.filter((e) => e.cont_dm["@content"][0]).length
logger.debugInfo(`Loaded ${result.music.length} songs from MDB file. ${gfCount} songs for GF, ${dmCount} songs for DM.`)
return result
}
export function gameVerToDataVer(ver: string): DATAVersion {
@ -72,18 +76,47 @@ export function gameVerToDataVer(ver: string): DATAVersion {
}
}
export async function processDataBuilder(gameVer: string, processHandler?: processRawDataHandler) {
const ver = gameVerToDataVer(gameVer)
const base = `data/mdb/${ver}`
if (IO.Exists(`${base}.b64`)) {
return await readB64JSON(`${base}.b64`);
/**
* Attempts to find a .json, .xml, or .b64 file (in that order) matching the given name in the specified folder.
* @param fileNameWithoutExtension - The name of the file to find (without the extension).
* @param path - The path to the folder to search. If left null, the default MDB folder ('data/mdb' in the plugin folder) will be used.
* @returns - The path of the first matching file found, or null if no file was found.
*/
export function findMDBFile(fileNameWithoutExtension: string, path: string = null): string {
path = path ?? mdbFolder
if (!IO.Exists(path)) {
throw `Path does not exist: ${path}`
}
const { music } = await readJSONOrXML(`${base}.json`, `${base}.xml`, processHandler ?? defaultProcessRawData)
// await IO.WriteFile(`${base}.b64`, Buffer.from(JSON.stringify({music})).toString("base64"))
return { music };
if (!path.endsWith("/")) {
path += "/"
}
for (const ext of allowedFormats) {
const filePath = path + fileNameWithoutExtension + ext
if (IO.Exists(filePath)) {
return filePath
}
}
return null
}
export async function defaultProcessRawData(path: string): Promise<CommonMusicData> {
export async function loadSongsForGameVersion(gameVer: string, processHandler?: processRawDataHandler) {
const ver = gameVerToDataVer(gameVer)
let mdbFile = findMDBFile(ver, mdbFolder)
if (mdbFile == null) {
throw `No valid MDB files were found in the data/mdb subfolder. Ensure that this folder contains at least one of the following: ${ver}.json, ${ver}.xml or ${ver}.b64`
}
const music = await readMDBFile(mdbFile, processHandler ?? defaultProcessRawXmlData)
return music
}
export async function defaultProcessRawXmlData(path: string): Promise<CommonMusicData> {
const data = await readXML(path)
const mdb = $(data).elements("mdb.mdb_data");
const music: any[] = [];
@ -106,7 +139,7 @@ export async function defaultProcessRawData(path: string): Promise<CommonMusicDa
id: K.ITEM('s32', m.number("music_id")),
cont_gf: K.ITEM('bool', gf == 0 ? 0 : 1),
cont_dm: K.ITEM('bool', dm == 0 ? 0 : 1),
is_secret: K.ITEM('bool', 0),
is_secret: K.ITEM('bool', m.number("is_secret", 0)),
is_hot: K.ITEM('bool', type == 2 ? 0 : 1),
data_ver: K.ITEM('s32', m.number("data_ver", 115)),
diff: K.ARRAY('u16', [

View File

@ -1,16 +1,17 @@
import { getVersion } from "../utils";
import { defaultProcessRawData, processDataBuilder } from "../data/mdb"
import { CommonMusicDataField, readJSONOrXML, readXML } from "../data/mdb";
import { CommonMusicDataField, findMDBFile, readMDBFile, loadSongsForGameVersion } from "../data/mdb";
import Logger from "../utils/logger"
const logger = new Logger("MusicList")
export const playableMusic: EPR = async (info, data, send) => {
const version = getVersion(info);
let music: CommonMusicDataField[] = [];
try {
if (U.GetConfig("enable_custom_mdb")) {
music = (await defaultProcessRawData('data/mdb/custom.xml')).music
let customMdb = findMDBFile("custom")
music = (await readMDBFile(customMdb)).music
}
} catch (e) {
logger.warn("Read Custom MDB failed. Using default MDB as a fallback.")
@ -19,10 +20,15 @@ export const playableMusic: EPR = async (info, data, send) => {
}
if (music.length == 0) {
music = _.get(await processDataBuilder(version), 'music', []);
music = (await loadSongsForGameVersion(version)).music
}
await send.object({
let response = getPlayableMusicResponse(music)
await send.object(response)
};
function getPlayableMusicResponse(music) {
return {
hot: {
major: K.ITEM('s32', 1),
minor: K.ITEM('s32', 1),
@ -30,5 +36,5 @@ export const playableMusic: EPR = async (info, data, send) => {
musicinfo: K.ATTR({ nr: `${music.length}` }, {
music,
}),
});
};
}
}

View File

@ -1,5 +1,7 @@
import { getEncoreStageData } from "../data/extrastage";
import Logger from "../utils/logger";
const logger = new Logger('info');
export const shopInfoRegist: EPR = async (info, data, send) => {
send.object({
data: {
@ -16,64 +18,8 @@ export const shopInfoRegist: EPR = async (info, data, send) => {
}
export const gameInfoGet: EPR = async (info, data, send) => {
const addition: any = {
monstar_subjugation: {
bonus_musicid: K.ITEM('s32', 0),
},
bear_fes: {},
nextadium: {},
};
const time = BigInt(31536000);
for (let i = 1; i <= 20; ++i) {
const obj = {
term: K.ITEM('u8', 0),
start_date_ms: K.ITEM('u64', time),
end_date_ms: K.ITEM('u64', time),
};
if (i == 1) {
addition[`phrase_combo_challenge`] = obj;
addition[`long_otobear_fes_1`] = {
term: K.ITEM('u8', 0),
start_date_ms: K.ITEM('u64', time),
end_date_ms: K.ITEM('u64', time),
bonus_musicid: {},
};
addition[`sdvx_stamprally3`] = obj;
addition[`chronicle_1`] = obj;
addition[`paseli_point_lottery`] = obj;
addition['sticker_campaign'] = {
term: K.ITEM('u8', 0),
sticker_list: {},
};
addition['thanksgiving'] = {
...obj,
box_term: {
state: K.ITEM('u8', 0)
}
};
addition['lotterybox'] = {
...obj,
box_term: {
state: K.ITEM('u8', 0)
}
};
} else {
addition[`phrase_combo_challenge_${i}`] = obj;
}
if (i <= 4) {
addition['monstar_subjugation'][`monstar_subjugation_${i}`] = obj;
addition['bear_fes'][`bear_fes_${i}`] = obj;
}
if (i <= 3) {
addition[`kouyou_challenge_${i}`] = {
term: K.ITEM('u8', 0),
bonus_musicid: K.ITEM('s32', 0),
};
}
}
const eventData = getEventDataResponse()
const extraData = getEncoreStageData(info)
await send.object({
@ -157,6 +103,70 @@ export const gameInfoGet: EPR = async (info, data, send) => {
},
},
},
...addition,
...eventData,
});
};
function getEventDataResponse() {
const addition: any = {
monstar_subjugation: {
bonus_musicid: K.ITEM('s32', 0),
},
bear_fes: {},
nextadium: {},
};
const time = BigInt(31536000);
for (let i = 1; i <= 20; ++i) {
const obj = {
term: K.ITEM('u8', 0),
start_date_ms: K.ITEM('u64', time),
end_date_ms: K.ITEM('u64', time),
};
if (i == 1) {
addition[`phrase_combo_challenge`] = obj;
addition[`long_otobear_fes_1`] = {
term: K.ITEM('u8', 0),
start_date_ms: K.ITEM('u64', time),
end_date_ms: K.ITEM('u64', time),
bonus_musicid: {},
};
addition[`sdvx_stamprally3`] = obj;
addition[`chronicle_1`] = obj;
addition[`paseli_point_lottery`] = obj;
addition['sticker_campaign'] = {
term: K.ITEM('u8', 0),
sticker_list: {},
};
addition['thanksgiving'] = {
...obj,
box_term: {
state: K.ITEM('u8', 0)
}
};
addition['lotterybox'] = {
...obj,
box_term: {
state: K.ITEM('u8', 0)
}
};
} else {
addition[`phrase_combo_challenge_${i}`] = obj;
}
if (i <= 4) {
addition['monstar_subjugation'][`monstar_subjugation_${i}`] = obj;
addition['bear_fes'][`bear_fes_${i}`] = obj;
}
if (i <= 3) {
addition[`kouyou_challenge_${i}`] = {
term: K.ITEM('u8', 0),
bonus_musicid: K.ITEM('s32', 0),
};
}
}
return addition
}

View File

@ -5,6 +5,8 @@ import { Record } from "../models/record";
import { Extra } from "../models/extra";
import { getVersion, isDM } from "../utils";
import { Scores } from "../models/scores";
import { PlayerStickerResponse } from "../models/playerstickerresponse";
import { SecretMusicResponse } from "../models/secretmusicresponse";
import { PLUGIN_VER } from "../const";
import Logger from "../utils/logger"
import { isAsphyxiaDebugMode } from "../Utils/index";
@ -230,38 +232,7 @@ export const getPlayer: EPR = async (info, data, send) => {
}));
}
const sticker: any[] = [];
if (_.isArray(name.card)) {
for (const item of name.card) {
const id = _.get(item, 'id');
const posX = _.get(item, 'position.0');
const posY = _.get(item, 'position.1');
const scaleX = _.get(item, 'scale.0');
const scaleY = _.get(item, 'scale.1');
const rotation = _.get(item, 'rotation');
if (
!isFinite(id) ||
!isFinite(posX) ||
!isFinite(posY) ||
!isFinite(scaleX) ||
!isFinite(scaleY) ||
!isFinite(rotation)
) {
continue;
}
sticker.push({
id: K.ITEM('s32', id),
pos_x: K.ITEM('float', posX),
pos_y: K.ITEM('float', posY),
scale_x: K.ITEM('float', scaleX),
scale_y: K.ITEM('float', scaleY),
rotate: K.ITEM('float', rotation),
});
}
}
const sticker: PlayerStickerResponse[] = getPlayerStickers(name.card);
const playerData: any = {
playerboard: {
@ -376,6 +347,7 @@ export const getPlayer: EPR = async (info, data, send) => {
}
const innerSecretMusic = getSecretMusicResponse(profile)
const innerFriendData = getFriendDataResponse(profile)
const response = {
player: K.ATTR({ 'no': `${no}` }, {
@ -392,7 +364,10 @@ export const getPlayer: EPR = async (info, data, send) => {
status: K.ARRAY('u32', extra.reward_status ?? Array(50).fill(0)),
},
rivaldata: {},
frienddata: {},
frienddata: {
friend: innerFriendData
},
thanks_medal: {
medal: K.ITEM('s32', 0),
grant_medal: K.ITEM('s32', 0),
@ -527,6 +502,42 @@ export const getPlayer: EPR = async (info, data, send) => {
send.object(response);
}
function getPlayerStickers(playerCard) : PlayerStickerResponse[] {
let stickers : PlayerStickerResponse[] = []
if (_.isArray(playerCard)) {
for (const item of playerCard) {
const id = _.get(item, 'id');
const posX = _.get(item, 'position.0');
const posY = _.get(item, 'position.1');
const scaleX = _.get(item, 'scale.0');
const scaleY = _.get(item, 'scale.1');
const rotation = _.get(item, 'rotation');
if (
!isFinite(id) ||
!isFinite(posX) ||
!isFinite(posY) ||
!isFinite(scaleX) ||
!isFinite(scaleY) ||
!isFinite(rotation)
) {
continue;
}
stickers.push({
id: K.ITEM('s32', id),
pos_x: K.ITEM('float', posX),
pos_y: K.ITEM('float', posY),
scale_x: K.ITEM('float', scaleX),
scale_y: K.ITEM('float', scaleY),
rotate: K.ITEM('float', rotation),
});
}
}
return stickers
}
async function getOrRegisterPlayerInfo(refid: string, version: string, no: number) {
let playerInfo = await DB.FindOne<PlayerInfo>(refid, {
collection: 'playerinfo',
@ -953,6 +964,8 @@ async function saveSinglePlayer(dataplayer: KDataReader, refid: string, no: numb
await DB.Upsert(refid, { collection: 'extra', game, version }, extra)
const playedStages = dataplayer.elements('stage');
// logStagesPlayed(playedStages)
const scores = await updatePlayerScoreCollection(refid, playedStages, version, game)
await saveScore(refid, version, game, scores);
}
@ -1122,8 +1135,8 @@ function parseSecretMusic(playerData: KDataReader) : SecretMusicEntry[]
return response
}
function getSecretMusicResponse(profile: Profile) {
let response = []
function getSecretMusicResponse(profile: Profile) : SecretMusicResponse[] {
let response : SecretMusicResponse[] = []
if (!profile.secretmusic?.music ) {
return response
@ -1140,3 +1153,19 @@ function getSecretMusicResponse(profile: Profile) {
return response
}
function getFriendDataResponse(profile: Profile) {
let response = []
return response;
}
function logStagesPlayed(playedStages: KDataReader[]) {
let result = "Stages played: "
for (let stage of playedStages) {
let id = stage.number('musicid')
result += `${id}, `
}
logger.debugLog(result)
}

View File

@ -25,14 +25,15 @@ export function register() {
name: "Dummy Encore for SPE (Nextage Only)",
desc: "Since Nextage's Special Premium Encore system is bit complicated, \n"
+ "SPE System isn't fully implemented. \n"
+ "This thing is bandage of these problem as limiting some Encores for SPE.",
+ "This option is a workaround for this issue as limiting some Encores for SPE.",
type: "boolean",
default: false
})
R.Config("enable_custom_mdb", {
name: "Enable Custom MDB",
desc: "For who uses own MDB. eg) Omnimix.",
desc: "If disabled, the server will provide the default MDB (song list) to Gitadora clients, depending on which version of the game they are running." +
"Enable this option to provide your own custom MDB instead. MDB files are stored in the 'gitadora@asphyxia/data/mdb' folder, and can be in .xml, .json or .b64 format.",
type: "boolean",
default: false,
})
@ -40,7 +41,7 @@ export function register() {
R.DataFile("data/mdb/custom.xml", {
accept: ".xml",
name: "Custom MDB",
desc: "You need to enable Custom MDB option first."
desc: "You need to enable the 'Enable Custom MDB' option for the uploaded file to have any effect."
})
R.WebUIEvent('updatePlayerInfo', updatePlayerInfo);

View File

@ -0,0 +1,13 @@
export interface CommonMusicDataField {
id: KITEM<"s32">;
cont_gf: KITEM<"bool">;
cont_dm: KITEM<"bool">;
is_secret: KITEM<"bool">;
is_hot: KITEM<"bool">;
data_ver: KITEM<"s32">;
diff: KARRAY<"u16">;
}
export interface CommonMusicData {
music: CommonMusicDataField[]
}

View File

@ -0,0 +1,8 @@
export interface PlayerStickerResponse {
id: KITEM<'s32'>,
pos_x: KITEM<'float'> ,
pos_y: KITEM<'float'>,
scale_x: KITEM<'float'> ,
scale_y: KITEM<'float'>,
rotate: KITEM<'float'>
}

View File

@ -2,4 +2,4 @@ export interface SecretMusicEntry {
musicid: number;
seq: number;
kind: number;
}
}

View File

@ -0,0 +1,5 @@
export interface SecretMusicResponse {
musicid: KITEM<'s32'>;
seq: KITEM<'u16'>;
kind: KITEM<'s32'>;
}

View File

@ -7,7 +7,6 @@ export default class Logger {
this.category = (category == null) ? null : `[${category}]`
}
public error(...args: any[]) {
this.argsHandler(console.error, ...args)
}
@ -18,7 +17,6 @@ export default class Logger {
}
}
public warn(...args: any[]) {
this.argsHandler(console.warn, ...args)
}
@ -29,7 +27,6 @@ export default class Logger {
}
}
public info(...args: any[]) {
this.argsHandler(console.info, ...args)
}
@ -40,7 +37,6 @@ export default class Logger {
}
}
public log(...args: any[]) {
this.argsHandler(console.log, ...args)
}
@ -51,7 +47,6 @@ export default class Logger {
}
}
private argsHandler(target: Function, ...args: any[]) {
if (this.category == null) {
target(...args)

View File

@ -1,6 +1,19 @@
//DATA//
info: DB.Find(refid, { collection: 'playerinfo' })
profile: DB.Find(refid, { collection: 'profile' })
-
-
function getFullGameName(shortName) {
switch (shortName) {
case "dm" :
return "Drummania"
case "gf":
return "Guitar Freaks"
default:
return "Unknown"
}
}
-
div
@ -33,4 +46,39 @@ div
button.button.is-primary(type="submit")
span.icon
i.mdi.mdi-check
span Submit
span Submit
div
each pr in profile
.card
.card-header
p.card-header-title
span.icon
i.mdi.mdi-account-details
| Profile Detail (#{getFullGameName(pr.game)} #{pr.version})
.card-content
form(method="post")
.field
label.label Skill
.control
input.input(type="text" name="skill", value=(pr.skill/100) readonly)
.field
label.label Skill (All Songs)
.control
input.input(type="text" name="all_skill", value=(pr.all_skill/100) readonly)
.field
label.label Stages Cleared
.control
input.input(type="text" name="clear_num", value=pr.clear_num readonly)
.field
label.label Full Combos
.control
input.input(type="text" name="full_num", value=pr.full_num readonly)
.field
label.label Excellent Full Combos
.control
input.input(type="text" name="exce_num", value=pr.exce_num readonly)
.field
label.label Sessions
.control
input.input(type="text" name="session_cnt", value=pr.session_cnt readonly)