Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions nostalgia@asphyxia/README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Nostalgia

Plugin Version: **v1.2.0**
Plugin Version: **v1.3.0**

Supported Versions
-------------------
- ノスタルジア/ First Version (Experiment-Old)
- Forte (Experiment-Old)
- Op.2
- Op.3 + omnimix(up to 08/07/2024)

About Experiment-Old Support
----------------------------
Expand All @@ -17,7 +18,11 @@ If you have a problem that move from old version to new version, There's webui f

Changelog
=========
1.2.0 (Current)
1.3.0
---------------
- Op.3 + omnimix(up to 08/07/2024) support

1.2.0
---------------
- Nostalgia First version support.

Expand Down
226 changes: 226 additions & 0 deletions nostalgia@asphyxia/data/Op3Music.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { readB64JSON, readXML } from './helper';

const OP3_XML_CANDIDATES = [
'data/op3_mdb.xml',
'data/music_list.xml',
];

const OP3_B64 = 'data/op3_mdb.json.b64';
const OP3_PROCESSED_B64 = 'data/op3_mdb_processed.json.b64';

export type ProcessedOp3Music = {
revision: string;
release_code: string;
music_spec: any[];
overwrite_spec: any[];
max_index: number;
};

type RawSong = Record<string, string | undefined>;

let processedMemo: ProcessedOp3Music | null = null;

function songNodes(data: any): any[] {
const list = data?.music_list;
if (!list) return [];

const specs = list.music_spec;
if (specs) return _.isArray(specs) ? specs : [specs];

const entries = _.isArray(list) ? list : [list];
if (entries.length === 1 && entries[0]?.music_spec) {
const inner = entries[0].music_spec;
return _.isArray(inner) ? inner : [inner];
}

const root = data.music_list?.['@attr'] ? data.music_list : data;
const children = Object.keys(root)
.filter((k) => k !== '@attr' && k !== 'music_spec')
.flatMap((k) => {
const v = root[k];
return _.isArray(v) ? v : v ? [v] : [];
});

if (children.length > 0) return children;
return [];
}

function readField(node: any, name: string, fallback = '0'): string {
const el = node?.[name];
if (el == null) return fallback;
if (typeof el === 'string' || typeof el === 'number') return `${el}`;
return `${_.get(el, '@content.0', fallback)}`;
}

function toSong(node: any): { index: string; fields: RawSong } {
const index = `${node?.['@attr']?.index ?? node?.index ?? '0'}`;
const fields: RawSong = {};
const keys = [
'priority', 'category_flag', 'primary_category',
'level_normal', 'level_hard', 'level_extreme', 'level_real',
'demo_popular', 'demo_bemani',
'destination_j', 'destination_a', 'destination_y', 'destination_k',
'offline', 'unlock_type', 'volume_bgm', 'volume_key',
'jk_jpn', 'jk_asia', 'jk_kor', 'jk_idn',
'real_unlock_type', 'real_once_price', 'real_forever_price',
];
for (const key of keys) {
fields[key] = readField(node, key);
}
return { index, fields };
}

function musicSpec(index: string, s: RawSong, overwrite = false) {
const bool = (k: string, def = '1') => K.ITEM('bool', readField({ [k]: s[k] }, k, def) !== '0');
const base: any = {
basename: K.ITEM('str', ''),
title: K.ITEM('str', ''),
title_kana: K.ITEM('str', ''),
artist: K.ITEM('str', ''),
artist_kana: K.ITEM('str', ''),
license: K.ITEM('str', ''),
license_site: K.ITEM('str', ''),
priority: K.ITEM('s8', parseInt(s.priority || '0', 10)),
category_flag: K.ITEM('s32', parseInt(s.category_flag || '0', 10)),
primary_category: K.ITEM('s8', parseInt(s.primary_category || '0', 10)),
level_normal: K.ITEM('s8', parseInt(s.level_normal || '0', 10)),
level_hard: K.ITEM('s8', parseInt(s.level_hard || '0', 10)),
level_extreme: K.ITEM('s8', parseInt(s.level_extreme || '0', 10)),
level_real: K.ITEM('s8', parseInt(s.level_real || '0', 10)),
demo_popular: bool('demo_popular', '0'),
demo_bemani: bool('demo_bemani', '0'),
destination_j: bool('destination_j'),
destination_a: bool('destination_a'),
destination_y: bool('destination_y'),
destination_k: bool('destination_k'),
offline: bool('offline', '0'),
unlock_type: K.ITEM('s8', parseInt(s.unlock_type || '0', 10)),
volume_bgm: K.ITEM('s8', parseInt(s.volume_bgm || '0', 10)),
volume_key: K.ITEM('s8', parseInt(s.volume_key || '0', 10)),
start_date: K.ITEM('str', '2017-03-01 10:00'),
end_date: K.ITEM('str', '9999-12-31 23:59'),
expiration_date: K.ITEM('str', '9999-12-31 23:59'),
description: K.ITEM('str', ''),
};

if (overwrite) {
return K.ATTR({ index }, {
jk_jpn: bool('jk_jpn'),
jk_asia: bool('jk_asia'),
jk_kor: bool('jk_kor'),
jk_idn: bool('jk_idn'),
unlock_type: K.ITEM('s8', parseInt(s.unlock_type || '0', 10)),
real_unlock_type: K.ITEM('s8', parseInt(s.real_unlock_type || '0', 10)),
start_date: K.ITEM('str', '2017-03-01 10:00'),
end_date: K.ITEM('str', '9999-12-31 23:59'),
real_once_price: K.ITEM('s32', parseInt(s.real_once_price || '300', 10)),
real_forever_price: K.ITEM('s32', parseInt(s.real_forever_price || '7500', 10)),
real_start_date: K.ITEM('str', '2017-03-01 10:00'),
real_end_date: K.ITEM('str', '9999-12-31 23:59'),
});
}

return K.ATTR({ index }, base);
}

function emptyProcessed(): ProcessedOp3Music {
return {
revision: '21261',
release_code: '2021090800',
music_spec: [],
overwrite_spec: [],
max_index: 0,
};
}

function buildProcessedFromRaw(raw: any): ProcessedOp3Music {
const attr = raw?.music_list?.['@attr'] ?? {};
const revision = `${attr.revision ?? '21261'}`;
const release_code = `${attr.release_code ?? '2021090800'}`;
const songs = songNodes(raw).map(toSong).filter((s) => parseInt(s.index, 10) > 0);

let maxIndex = 0;
for (const s of songs) {
maxIndex = Math.max(maxIndex, parseInt(s.index, 10));
}

return {
revision,
release_code,
music_spec: songs.map((s) => musicSpec(s.index, s.fields, false)),
overwrite_spec: songs.map((s) => musicSpec(s.index, s.fields, true)),
max_index: maxIndex,
};
}

async function readProcessedCache(): Promise<ProcessedOp3Music | null> {
if (!IO.Exists(OP3_PROCESSED_B64)) {
return null;
}
try {
return await readB64JSON(OP3_PROCESSED_B64);
} catch {
return null;
}
}

async function writeProcessedCache(data: ProcessedOp3Music): Promise<void> {
await IO.WriteFile(
OP3_PROCESSED_B64,
Buffer.from(JSON.stringify(data)).toString('base64')
);
}

async function loadRaw(): Promise<any | null> {
if (IO.Exists(OP3_B64)) {
return readB64JSON(OP3_B64);
}

let xmlPath: string | null = null;
for (const candidate of OP3_XML_CANDIDATES) {
if (IO.Exists(candidate)) {
xmlPath = candidate;
break;
}
}

if (!xmlPath) {
console.warn('[nostalgia@asphyxia] OP3 music DB missing. Copy music_list.xml to data/op3_mdb.xml');
return null;
}

const raw = await readXML(xmlPath);
await IO.WriteFile(
OP3_B64,
Buffer.from(JSON.stringify(raw)).toString('base64')
);
return raw;
}

export async function processOp3MusicData(): Promise<ProcessedOp3Music & { fromCache: boolean }> {
if (processedMemo) {
return { ...processedMemo, fromCache: true };
}

const raw = await loadRaw();
if (!raw) {
const empty = emptyProcessed();
processedMemo = empty;
return { ...empty, fromCache: false };
}

const attr = raw?.music_list?.['@attr'] ?? {};
const revision = `${attr.revision ?? '21261'}`;
const release_code = `${attr.release_code ?? '2021090800'}`;

const cached = await readProcessedCache();
if (cached && cached.revision === revision && cached.release_code === release_code) {
processedMemo = cached;
return { ...cached, fromCache: true };
}

const built = buildProcessedFromRaw(raw);
processedMemo = built;
await writeProcessedCache(built);
return { ...built, fromCache: false };
}
1 change: 1 addition & 0 deletions nostalgia@asphyxia/data/op3_mdb.json.b64

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions nostalgia@asphyxia/data/op3_mdb_processed.json.b64

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions nostalgia@asphyxia/handler/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import { processData as firstData } from "../data/FirstMusic";
import { processData as forteData } from "../data/ForteMusic";
import { processOp3MusicData } from "../data/Op3Music";
import { readB64JSON } from "../data/helper";
import { NosVersionHelper } from "../utils";

Expand Down Expand Up @@ -81,9 +82,74 @@ export const get_common_info = async (info, data, send) => {
});
};

let op3MusicInfoByteCache: { data: any; xCompress: any; xCorePlugin: any } | null = null;

export const get_music_info: EPR = async (info, data, send) => {
const version = new NosVersionHelper(info)

if (version.isOp3()) {
const t0 = Date.now();
const op3 = await processOp3MusicData();
console.log(
`[nostalgia@asphyxia] op3_common.get_music_info ${Date.now() - t0}ms (songs=${op3.max_index}, cache=${op3.fromCache ? 'hit' : 'miss'})`
);

try {
const inst: any = send as any;
const res: any = inst?.res;
if (!inst?.body?.encrypted && res && typeof res.send === 'function' && typeof res.setHeader === 'function') {
if (op3MusicInfoByteCache) {
res.setHeader('X-Compress', op3MusicInfoByteCache.xCompress ?? 'none');
if (op3MusicInfoByteCache.xCorePlugin != null) {
res.setHeader('X-CORE-Plugin', op3MusicInfoByteCache.xCorePlugin);
}
res.send(op3MusicInfoByteCache.data);
inst.sent = true;
console.log(`[nostalgia@asphyxia] op3_common.get_music_info byte-cache HIT (songs=${op3.max_index})`);
return;
}
const realSend = res.send.bind(res);
res.send = (d: any) => {
res.send = realSend;
try {
if (Buffer.isBuffer(d) || typeof d === 'string') {
op3MusicInfoByteCache = {
data: d,
xCompress: res.getHeader('X-Compress'),
xCorePlugin: res.getHeader('X-CORE-Plugin'),
};
}
} catch (_e) { /* ignore capture failure */ }
return realSend(d);
};
}
} catch (_e) { /* fall back to normal send.object */ }

send.object({
permitted_list,
music_list: K.ATTR({
revision: op3.revision,
release_code: op3.release_code,
}, {
music_spec: op3.music_spec,
}),
overwrite_music_list: K.ATTR({
revision: op3.revision,
release_code: op3.release_code,
}, {
music_spec: op3.overwrite_spec,
}),
gamedata_flag_list: {},
trend_music_list: {
trend_music: K.ATTR({ music_index: '1', rank: '1' }, {}),
},
olupdate: {
delete_flag: K.ITEM('bool', 0),
},
});
return;
}

const music_spec: any = [];
for (let i = 1; i < 400; ++i) {
music_spec.push(K.ATTR({ index: `${i}` }, {
Expand Down
Loading