ddr working now

This commit is contained in:
GEEKiDoS
2025-04-11 17:29:42 +08:00
parent bfc994a8ff
commit 9f92ff5056
15 changed files with 1476 additions and 297 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@laochan/ea3",
"name": "@laochan/eacloud",
"version": "0.0.0",
"description": "Yet another e-amusement server emulator.",
"description": "EACloud server emulator.",
"type": "module",
"engines": {
"node": ">= 20.9 < 21"
+90 -22
View File
@@ -1,13 +1,23 @@
import { singleton } from "tsyringe";
import { Combine } from "../../utils/combine.js";
import { generic } from "../../decorators/eacnet.js";
import { Serializable, v } from "../../utils/kxml-value.js";
import config from "../../utils/config.js";
import { EacGeneric } from "../eac-generic.js";
import * as data from '../../datas/ddr.js'
import { inject, singleton } from 'tsyringe';
import { Combine } from '../../utils/combine.js';
import { generic } from '../../decorators/eacnet.js';
import { Serializable, v } from '../../utils/kxml-value.js';
import { EacGeneric } from '../eac-generic.js';
import * as data from '../../datas/ddr.js';
import { User } from './user.js';
import { UserService } from '../../services/ddr/user.js';
import { Context } from '../../types.js';
@singleton()
export default class extends Combine(EacGeneric) {
export default class extends Combine(User, EacGeneric) {
constructor(
@inject(UserService) private readonly userService: UserService,
) {
super();
this.userService;
}
@generic()
async getItemList(): Promise<Serializable> {
return {
@@ -17,11 +27,27 @@ export default class extends Combine(EacGeneric) {
item_num: v.s32(1),
item: {
item_id: v.str('DDR_TICKET'),
not_free_count: v.s32(0),
free_count: v.s32(114514),
buyable_point: v.s32(0),
not_free_count: v.s32(114514),
free_count: v.s32(0),
buyable_point: v.s32(9999999),
},
}
},
};
}
@generic()
async tabooword_2_check(): Promise<Serializable> {
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
tabooword_2: {
$status: 0,
is_taboo: v.bool(false),
},
},
};
}
@@ -35,12 +61,11 @@ export default class extends Combine(EacGeneric) {
response: {
log_2: {
$status: 0,
}
}
}
},
},
};
}
// Get Preview
@generic()
async getprev(): Promise<Serializable> {
return {
@@ -51,9 +76,28 @@ export default class extends Combine(EacGeneric) {
response: {
fileinfo_2: {
$status: 0,
soundinfo: data.soundInfos,
}
}
soundinfo: data.previewSoundInfos,
},
},
};
}
@generic()
async getmain(ctx: Context): Promise<Serializable> {
const mcode = ctx.body.data.mcode as number;
const result = data.mainSoundInfoMap.get(mcode);
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
fileinfo_2: {
$status: 0,
soundinfo: result,
},
},
};
}
@@ -69,9 +113,33 @@ export default class extends Combine(EacGeneric) {
$status: 0,
mdb: {
music: data.musics,
}
}
}
},
},
},
};
}
@generic()
async getGoodsList(): Promise<Serializable> {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
goods_num: v.s32(3),
goods: [{
goods_id: v.str('G0000001'),
item_id: v.str('DDR_TICKET'),
price: v.s32(1),
}, {
goods_id: v.str('G0000002'),
item_id: v.str('DDR_TICKET'),
price: v.s32(1),
}, {
goods_id: v.str('G0000003'),
item_id: v.str('DDR_TICKET'),
price: v.s32(1),
}],
},
};
}
}
+276 -33
View File
@@ -1,59 +1,205 @@
import { generic } from "../../decorators/eacnet.js";
import { Context } from "../../types.js";
import { Serializable, v } from "../../utils/kxml-value.js";
import { musicPackMax } from "../../datas/ddr.js";
import { constants } from "node:fs";
import { generic } from '../../decorators/eacnet.js';
import { Context } from '../../types.js';
import { Serializable, v } from '../../utils/kxml-value.js';
import { mpacks } from '../../datas/ddr.js';
import { PlayerRecordType, UserLoadReq, UserSaveReq } from '../../types/ddr/index.js';
import { UserService } from '../../services/ddr/user.js';
export class User {
userService: UserService;
async _usernew(token: string): Promise<Serializable> {
return {};
await this.userService.upsertPlayData({
_id: token,
name: '',
bestClearLevel: 0,
singleGrade: 0,
doubleGrade: 0,
leagueClass: 0,
records: {
'COMMON': new Array(64).fill(''),
'OPTION': new Array(64).fill(''),
'LAST': new Array(64).fill(''),
'RIVAL': new Array(64).fill(''),
},
})
return {
$status: 0,
result: v.s32(0),
seq: v.s32(0),
code: v.s32(0),
shoparea: v.str('JP-13'),
};
}
async _userload(token: string): Promise<Serializable> {
return {};
emptyData() {
return {
$status: 0,
is_new: v.bool(true),
is_refid_locked: v.bool(false),
best_clear_level: v.s8(0),
eventdata_count_all: v.s16(0),
grade: {
single_grade: v.u32(0),
double_grade: v.u32(0),
},
golden_league: {
league_class: v.s32(0),
},
gamesession: v.u64(Date.now()),
result: v.s32(0),
};
}
async _ghostload(token: string): Promise<Serializable> {
return {};
async _userload(token: string, data: UserLoadReq): Promise<Serializable> {
if (data.refid.startsWith('X')) {
return this.emptyData();
}
const playData = await this.userService.findPlayData(token);
if (!playData) {
return this.emptyData();
}
const musicDatas = await this.userService.findMusicDatas(token);
return {
$status: 0,
is_new: v.bool(false),
is_refid_locked: v.bool(false),
best_clear_level: v.s8(playData.bestClearLevel),
eventdata_count_all: v.s16(0),
grade: {
single_grade: v.u32(playData.singleGrade),
double_grade: v.u32(playData.doubleGrade),
},
golden_league: {
league_class: v.s32(playData.leagueClass),
},
gamesession: v.u64(Date.now()),
music: musicDatas.map(m => ({
mcode: v.u32(m.mcode),
note_comp: m.notes.map(n => ({
note_str: v.str(n.join(',')),
})),
})),
result: v.s32(0),
};
}
async _usersave(token: string, data: any): Promise<Serializable> {
return {};
async _ghostload(): Promise<Serializable> {
return {
$status: 0,
result: v.s32(0),
};
}
async _rivalload(token: string, data: any): Promise<Serializable> {
return {};
async _usersave(token: string, data: UserSaveReq): Promise<Serializable> {
const playData = await this.userService.findPlayData(token);
if (!playData) {
return {
$status: 404,
result: v.s32(1),
};
}
playData.name = data.name;
playData.records['COMMON'][24] = data.name;
const updateMusics = data.note.filter(n => n.mcode != 0)
await Promise.all([
this.userService.upsertPlayData(playData),
this.userService.insertPlayRecords(updateMusics.map(m => ({
player: token,
mcode: m.mcode,
noteType: m.notetype,
playtime: m.endtime,
playStyle: m.playstyle,
rank: m.rank,
clearKind: m.clearkind,
score: m.score,
exScore: m.score,
maxCombo: m.maxcombo,
life: m.life,
judge: {
fast: m.fastcount,
slow: m.slowcount,
marvelous: m.judge_marvelous,
great: m.judge_great,
good: m.judge_good,
boo: m.judge_boo,
miss: m.judge_miss,
ok: m.judge_ok,
ng: m.judge_ng,
},
calorie: m.calorie,
ghost: m.ghost,
}))),
...updateMusics.map(m => this.userService.updateMusicData(token, m))
]);
return {
$status: 0,
result: v.s32(0),
};
}
async _inheritance(token: string, data: any): Promise<Serializable> {
return {};
async _rivalload(): Promise<Serializable> {
return {
$status: 0,
result: v.s32(0),
};
}
async _arcadeload(token: string, data: any): Promise<Serializable> {
return {};
async _inheritance(): Promise<Serializable> {
return {
$status: 0,
result: v.s32(0),
};
}
async _linkage_pre_playable(token: string, data: any): Promise<Serializable> {
return {};
async _arcadeload(): Promise<Serializable> {
return {
$status: 0,
is_acdata_exists: v.bool(false),
eventload: {
eventdata_count_all: v.s16(0),
},
acdata_gettime: v.u64(Date.now()),
result: v.s32(0),
};
}
async _linkage_pre_playable(): Promise<Serializable> {
const now = new Date();
return {
$status: 0,
update_y: v.u16(now.getUTCFullYear()),
update_m: v.u8(now.getUTCMonth()),
next_y: v.u16(now.getUTCFullYear() + 1),
next_m: v.u8(now.getUTCMonth()),
result: v.s32(0),
};
}
async _mpackload() {
return {
mpacklist: new Array(musicPackMax).map(() => ({
pre_playable_id: v.str(''),
permit_freeplay: v.bool(false),
is_purchased: v.bool(true),
})),
}
$status: 0,
mpacklist: mpacks,
result: v.s32(0),
};
}
@generic()
async usergamedata_advanced(ctx: Context): Promise<Serializable> {
const { data: { mode } } = ctx.body as {
data: {
mode: string,
refid: string,
}
const data = ctx.body.data as {
mode: string,
};
return {
@@ -72,8 +218,105 @@ export class User {
'arcadeload': this._arcadeload,
'mpackload': this._mpackload,
'linkage_pre_playable': this._linkage_pre_playable,
}[mode].apply(this, [ctx.token, ctx.body]),
}
}[data.mode].apply(this, [ctx.token, data]),
},
};
}
convertCsvMask(maskString: string): number[] {
const mask = BigInt('0x' + maskString);
const fields = [];
for (let i = 0; i < 64; i++) {
if ((mask & (1n << BigInt(i))) == 0n)
continue;
fields.push(i);
}
return fields;
}
@generic()
async usergamedata_send(ctx: Context): Promise<Serializable> {
const playerData = await this.userService.findPlayData(ctx.token);
const records = ((ctx.body.data.record.d instanceof Array ?
ctx.body.data.record.d : [ctx.body.data.record.d]) as string[])
.map(v => {
const columns = atob(v).split(',')
const mask = this.convertCsvMask(columns[0]);
const type = columns[1] as PlayerRecordType;
const fields = columns.slice(3);
return {
mask, type, fields,
}
});
for (const record of records) {
const row = playerData.records[record.type] ?? (new Array<string>(32).fill(''));
record.mask.forEach((col, i) => {
row[col] = record.fields[i];
})
}
await this.userService.upsertPlayData(playerData);
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
playerdata_2: {
$status: 0,
},
},
};
}
@generic()
async usergamedata_recv(ctx: Context): Promise<Serializable> {
const csv = ctx.body.data.recv_csv as string;
const loadFields = csv.split(',');
const result: Serializable[] = [];
const playData = await this.userService.findPlayData(ctx.token);
for (let i = 0; i < loadFields.length; i += 2) {
const mask = this.convertCsvMask(loadFields[i + 1]);
const type = loadFields[i] as PlayerRecordType;
if (!(playData && type in playData.records)) {
result.push(v.str('<NODATA>'));
continue;
}
const row = ['1'];
mask.forEach((col) => {
row.push(playData.records[type][col]);
});
row.push('');
result.push(v.str(btoa(row.join(','))));
}
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
playerdata_2: {
$status: 0,
player: {
record_num: v.u32(loadFields.length),
record: {
d: result,
},
},
},
},
};
}
}
+57 -21
View File
@@ -1,8 +1,8 @@
import { generic } from "../decorators/eacnet.js";
import { Context } from "../types.js";
import config from "../utils/config.js";
import { Serializable, v } from "../utils/kxml-value.js";
import { tokenToCardNumber, tokenToHash, tokenToSnsId } from "../utils/laochan-id.js";
import { generic } from '../decorators/eacnet.js';
import { Context } from '../types.js';
import config from '../utils/config.js';
import { Serializable, v } from '../utils/kxml-value.js';
import { tokenToCardNumber, tokenToHash, tokenToSnsId } from '../utils/laochan-id.js';
export class EacGeneric {
@generic()
@@ -12,7 +12,7 @@ export class EacGeneric {
error_code: v.s32(0),
response: {
goods_num: v.s32(0),
}
},
};
}
@@ -26,7 +26,7 @@ export class EacGeneric {
ref_id: v.str(tokenToHash(ctx.token)),
data_id: v.str(tokenToHash(ctx.token)),
sns_id: v.str(tokenToSnsId(ctx.token)),
}
},
};
}
@@ -37,14 +37,14 @@ export class EacGeneric {
error_code: v.s32(0),
response: {
subscription: {
name: v.str([
'eac_ddr_basic',
'EAC_POPNLIVELY',
'eac_sdvx_basic',
'eacgitadora_basic'
]),
name: [
v.str('eac_ddr_basic'),
v.str('EAC_POPNLIVELY'),
v.str('eac_sdvx_basic'),
v.str('eacgitadora_basic'),
],
},
}
},
};
}
@@ -61,7 +61,7 @@ export class EacGeneric {
mainte_end_clock: v.u64(0),
usta_boot_status: v.s32(1),
usta_time_remain: v.s32(0),
}
},
};
}
@@ -72,7 +72,7 @@ export class EacGeneric {
error_code: v.s32(0),
response: {
permission: v.s32(1),
}
},
};
}
@@ -84,8 +84,8 @@ export class EacGeneric {
error_code: v.s32(0),
response: {
server_clock: v.u64(new Date().valueOf()),
}
}
},
};
}
@generic()
@@ -97,8 +97,44 @@ export class EacGeneric {
server_state: v.s32(1),
mainte_start_clock: v.u64(0),
mainte_end_clock: v.u64(0),
}
}
},
};
}
_transactionIdMap: Map<string, string> = new Map<string, string>();
@generic()
async reserveConsumeItem(ctx: Context): Promise<Serializable> {
const goodsId = ctx.body.goodsId as string;
const transactionId = (Math.random() * 10000000000).toString(16);
this._transactionIdMap.set(transactionId, goodsId);
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
transaction_id: v.str(transactionId),
},
};
}
@generic()
async consumeItem(ctx: Context): Promise<Serializable> {
const transactionId = ctx.body.transaction_id as string;
const goodsId = this._transactionIdMap.get(transactionId);
this._transactionIdMap.delete(transactionId);
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
item_id: v.str(goodsId),
free_count: v.s32(0),
not_free_count: v.s32(1919810),
},
};
}
@generic()
@@ -183,7 +219,7 @@ export class EacGeneric {
},
],
}
},
};
}
}
-14
View File
@@ -25,17 +25,3 @@ export const ITEM_LIST = data.items.map(id => ({
free_count: v.s32(0),
}));
export const MUSIC_LIST = [
...data.musics.map(m => ({
music_id: v.s32(m.id),
kind: v.s32(1),
note_bit: v.s32(m.noteBit),
music_pack_item_id: v.str('')
})),
...data.omni_musics.map(m => ({
music_id: v.s32(m.id),
kind: v.s32(2),
note_bit: v.s32(0),
music_pack_item_id: v.str('')
})),
];
+4 -26
View File
@@ -3,9 +3,7 @@ import { p2d } from "../../decorators/eacnet.js";
import { UserService } from "../../services/p2d/user.js";
import { Context } from "../../types.js";
import { v } from "../../utils/kxml-value.js";
import { MUSIC_LIST } from "./index.js";
import { Binary } from "mongodb";
import * as data from '../../datas/p2d.js'
// user only needs to unlock omni music, which is temporary
const userUnlockMusic = new Map<string, number>();
@@ -90,12 +88,10 @@ export class Music {
const result = ctx.body as PlayerPlayLog;
result.player = ctx.token;
let updateTask = Promise.resolve();
const play_style = Math.floor(result.note_id / 5);
const diff = result.note_id % 5;
updateTask = (async () => {
const updateTask = (async () => {
const musicData = await this.userService.getMusicData(result.player, result.music_id, play_style);
musicData.play_num[diff]++;
@@ -117,7 +113,7 @@ export class Music {
musicData.clear_num[diff]++;
}
this.userService.upsertMusicData(musicData);
await this.userService.upsertMusicData(musicData);
})();
// @ts-expect-error ghost is Buffer now, convert it to binary
@@ -148,24 +144,7 @@ export class Music {
}
@p2d()
async getMusicList(ctx: Context) {
let music = [...MUSIC_LIST];
if (userUnlockMusic.has(ctx.token)) {
const targetMusic = userUnlockMusic.get(ctx.token);
userUnlockMusic.delete(ctx.token);
const bitData = data.omni_musics.find(v => v.id === targetMusic);
music = music.filter(v => v.music_id['__value'] !== targetMusic);
music.push({
music_id: v.s32(bitData.id),
kind: v.s32(1),
note_bit: v.s32(bitData.noteBit),
music_pack_item_id: v.str(''),
});
}
async getMusicList() {
return {
status: v.s32(0),
error: v.s32(0),
@@ -173,8 +152,7 @@ export class Music {
status: v.s32(0),
check_sum: v.str(''),
music_list: {
music_num: v.s32(music.length),
music,
music_num: v.s32(0),
}
},
};
-1
View File
@@ -1,5 +1,4 @@
import * as data from '../../datas/sdvx.js';
import { tokenToCardNumber, tokenToHash, tokenToSnsId } from "../../utils/laochan-id.js";
import { Context } from "../../types.js";
import { generic } from '../../decorators/eacnet.js';
import { KValueG, Serializable, ValueTypes, v } from '../../utils/kxml-value.js';
+798 -159
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -7,6 +7,7 @@ export function eacnet(protocol_name: string, topName: string, dataOffset: numbe
const orig = descriptor.value as Function;
descriptor.value = async function (...args: any[]) {
const obj = await orig.apply(this, args);
const kxml = toKBinXml(topName, obj, encoding);
const compressed = LZ77.compress(kxml.data);
const result = Buffer.allocUnsafe(compressed.length + dataOffset);
+2 -2
View File
@@ -62,8 +62,8 @@ async function main(): Promise<void> {
.use(eacnet)
.use(async (ctx, next) => {
ctx.logger = ctx.service
? register(`logger:${ctx.service.name}:${ctx.service.method}`, {
useValue: new Logger(`${ctx.service.name}:${ctx.service.method}`),
? register(`logger:${ctx.service.name}:${ctx.service.module ? ctx.service.module + ':' : '' }${ctx.service.method}`, {
useValue: new Logger(`${ctx.service.name}:${ctx.service.module ? ctx.service.module + ':' : '' }${ctx.service.method}`),
})
: logger;
+2 -2
View File
@@ -40,12 +40,12 @@ export async function eacnet(ctx: Context, next: Next): Promise<any> {
ctx.service = {
name: info.game_id,
module: info.module,
module: request.module,
method: request.method,
};
ctx.body = request.data ?? {};
ctx.eacnetRequest = request;
ctx.eacnetRequest = request.service ? request : undefined;
return next();
}
+70
View File
@@ -0,0 +1,70 @@
import { inject, injectable } from 'tsyringe';
import { Db } from 'mongodb';
import { NoteRecord, PlayerMusicData, PlayerPlayData, PlayerPlayRecord } from '../../types/ddr/index.js';
@injectable()
export class UserService {
constructor(
@inject(Db)
private readonly db: Db,
) {
}
get playDataCol() {
return this.db.collection<PlayerPlayData>('ddr_play_data');
}
get playerMusicCol() {
return this.db.collection<PlayerMusicData>('ddr_music_data');
}
get playRecordCol() {
return this.db.collection<PlayerPlayRecord>('ddr_play_record');
}
findPlayData(token: string): Promise<PlayerPlayData> {
return this.playDataCol.findOne({ _id: token });
}
async upsertPlayData(data: PlayerPlayData): Promise<void> {
await this.playDataCol.updateOne({ _id: data._id }, {
$set: data,
}, { upsert: true });
}
async insertPlayRecords(records: PlayerPlayRecord[]): Promise<void> {
if (!records.length) {
return;
}
await this.playRecordCol.insertMany(records, {
ordered: false,
});
}
async updateMusicData(player: string, record: NoteRecord): Promise<void> {
const data = await this.playerMusicCol.findOne({ player, mcode: record.mcode }) ?? {
player,
mcode: record.mcode,
notes: [],
} as PlayerMusicData;
while (data.notes.length < record.notetype + 1) {
data.notes.push([0, 0, 0, 0, 0]);
}
let [playCount, rank, clearKind, score, unk] = data.notes[record.notetype];
playCount++;
rank = Math.max(record.rank, rank);
clearKind = Math.max(record.clearkind, clearKind);
score = Math.max(record.score, score);
data.notes[record.notetype] = [playCount, rank, clearKind, score, unk];
await this.playerMusicCol.updateOne({ player, mcode: record.mcode }, { $set: data }, { upsert: true });
}
findMusicDatas(player: string): Promise<PlayerMusicData[]> {
return this.playerMusicCol.find({ player }).toArray();
}
}
+44
View File
@@ -0,0 +1,44 @@
import { PlayerRecordType } from "./index.js";
export interface PlayerPlayData {
_id: string;
name: string;
bestClearLevel: number;
singleGrade: number;
doubleGrade: number;
leagueClass: number;
records: Record<PlayerRecordType, string[]>;
}
export interface PlayerMusicData {
player: string;
mcode: number;
notes: number[][];
}
export interface PlayerPlayRecord {
player: string;
mcode: number;
noteType: number;
playtime: number;
playStyle: number;
rank: number;
clearKind: number;
score: number
exScore: number;
maxCombo: number;
life: number;
judge: {
fast: number;
slow: number;
marvelous: number;
great: number;
good: number;
boo: number;
miss: number;
ok: number;
ng: number
};
calorie: number;
ghost: string;
}
+114 -12
View File
@@ -1,15 +1,117 @@
export interface PlayerPlayData {
_id: string;
name: string;
bestClearLevel: number;
singleGrade: number;
doubleGrade: number;
leagueClass: number;
records: string[];
export * from './db.js'
export type PlayerRecordType = 'COMMON' | 'OPTION' | 'LAST' | 'RIVAL';
export interface EventRecord {
eventid: number;
eventtype: number;
eventno: number;
comptime: number;
savedata: number;
}
export interface PlayerMusicData {
player: string;
mcode: number;
notes: string[];
export interface NoteRecord {
stagenum: number
mcode: number
notetype: number
rank: number
clearkind: number
score: number
exscore: number
maxcombo: number
life: number
fastcount: number
slowcount: number
judge_marvelous: number
judge_perfect: number
judge_great: number
judge_good: number
judge_boo: number
judge_miss: number
judge_ok: number
judge_ng: number
calorie: number
ghostsize: number
ghost: string
opt_speed: number
opt_boost: number
opt_appearance: number
opt_turn: number
opt_dark: number
opt_scroll: number
opt_arrowcolor: number
opt_cut: number
opt_freeze: number
opt_jump: number
opt_arrowshape: number
opt_filter: number
opt_guideline: number
opt_gauge: number
opt_judgepriority: number
opt_timing: number
basename: string
title_b64: string
artist_b64: string
bpmMax: number
bpmMin: number
level: number
series: number
bemaniFlag: number
genreFlag: number
limited: number
region: number
gr_voltage: number
gr_stringeam: number
gr_chaos: number
gr_freeze: number
gr_air: number
share: boolean
endtime: number
folder: number
playstyle: number
measurement: {
skipped_level: number
measured_point: string
measured_level: string
measured_level_result: number
recommended: {
mcode: number
notetype: number
priority: number
}[];
}
}
export interface UserGameDataReq {
mode: string;
refid: string;
pcbid: string;
locid: string;
shoparea: string;
gamesession: string;
}
export interface UserSaveReq extends UserGameDataReq {
name: string;
ddrcode: number;
playide: number;
playstyle: number;
area: number;
weight100: number;
shopname: string;
ispremium: boolean;
iseapass: boolean;
istakeover: boolean;
isrepeater: boolean;
isgameover: boolean;
note: NoteRecord[];
event: EventRecord[];
}
export interface ArcadeLoadReq extends UserGameDataReq {
ac_releasecode: string;
}
export interface UserLoadReq extends UserGameDataReq {
is_mat: boolean;
}
+16 -3
View File
@@ -2,7 +2,6 @@ import { to_bin, to_xml } from "@kamyu/kbinxml";
import { XMLParser } from "fast-xml-parser";
import _ from "lodash";
import { Serializable } from "./kxml-value.js";
import { writeFileSync } from "fs";
export const parser = new XMLParser({
ignoreAttributes: false,
@@ -135,10 +134,24 @@ function serializeValue(value: any, type: string): string {
return value.toString('hex');
}
if (['ip4', 'str', 'string'].includes(type)) {
if (['ip4'].includes(type)) {
return value;
}
if (['str', 'string'].includes(type)) {
return value.replace(/[<>&'"]/g, (ch: string) => {
switch (ch) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '\'': return '&apos;';
case '"': return '&quot;';
}
return ch;
});
}
if (type == 'time') {
if (value instanceof Date) {
return Math.floor(value.valueOf() / 1000).toString();
@@ -228,6 +241,6 @@ function serializeObject(obj: Serializable, name: string, linePrefix: string = '
export function toKBinXml(topName: string, obj: Serializable, encoding: 'UTF-8' | 'SHIFT_JIS' = 'UTF-8') {
const xml = `<?xml version="1.0" encoding="${encoding}"?>` + serializeObject(obj, topName);
writeFileSync('dump.xml', xml);
console.log(xml);
return to_bin(xml);
}