pass sdvx requests to maomani

This commit is contained in:
GEEKiDoS
2025-04-14 16:13:17 +08:00
parent a47162655a
commit c8824eec5d
14 changed files with 635 additions and 3192 deletions
+350 -385
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -47,7 +47,8 @@
"license": "MIT",
"dependencies": {
"@cordisjs/logger": "^0.1.3",
"@kamyu/kbinxml": "^2.0.2",
"@geekidos/kbinxml": "^2.0.4",
"@kamyu/lz77": "^1.0.0",
"@koa/cors": "^5.0.0",
"@koa/router": "^12.0.1",
"arc4": "^2.1.4",
@@ -59,6 +60,7 @@
"koa-bodyparser": "^4.4.1",
"lodash": "^4.17.21",
"mongodb": "^6.4.0",
"node-cache": "^5.1.2",
"pug": "^3.0.2",
"reflect-metadata": "^0.2.1",
"tslib": "~2.6",
+2 -2
View File
@@ -25,11 +25,11 @@ export class DefaultService {
};
// handle ac relay
if (ctx.eacnetRequest) {
if (ctx.acRelayInfo) {
result['xrpc_status_code'] = v.s32(0);
result['xrpc_fault_code'] = v.s32(0);
result['response'] = {
[ctx.eacnetRequest.module]: {
[ctx.acRelayInfo.module]: {
$status: 0,
}
}
+54 -79
View File
@@ -2,100 +2,75 @@ import { generic } from "../../decorators/eacnet.js";
import { Context } from "../../types.js";
import { Serializable, v } from "../../utils/kxml-value.js";
import * as data from '../../datas/sdvx.js';
import { requestEa3 } from "../../utils/ea3.js";
import { SDVX_AC_MODEL } from "./index.js";
import { tokenToCardNumber } from '../../utils/laochan-id.js';
import { cache } from '../../decorators/cache.js';
export class AcRelay {
@generic()
async sv6_play_e(): Promise<Serializable> {
async acRelayCommon(ctx: Context): Promise<Serializable> {
const { status, response } = await requestEa3(ctx.acRelayInfo, SDVX_AC_MODEL, ctx.token);
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: {
$status: 0,
},
}
xrpc_status_code: v.s32(status),
xrpc_fault_code: v.s32(status),
response,
};
}
@generic()
async sv6_play_s(): 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: {
game: {
$status: 0,
play_id: v.u32(Math.floor(new Date().valueOf() / 6e4)),
},
}
};
async sv6_play_e(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
@generic()
async sv6_hiscore() {
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: {
$status: 0,
sc: {},
}
}
};
async sv6_play_s(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
@generic()
async sv6_log(): 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: {
game: {
$status: 0,
}
}
};
async sv6_hiscore(ctx: Context) {
return this.acRelayCommon(ctx);
}
@generic()
async sv6_common(): 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: {
game: {
$status: 0,
music: {},
event: {
info: data.eventList.map(id => ({
event_id: v.str(id),
}))
},
extend: {
info: data.extendInfos,
},
music_limited: {
info: data.musicInfos,
},
skill_course: {
info: data.skillCourses,
},
appealcard: {},
valgene: {},
}
}
}
async sv6_log(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
@cache('sv6_common')
async sv6_common(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
async sv6_save_m(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
async sv6_load_r(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
async sv6_load_m(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
async sv6_save_c(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
async sv6_save(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
async sv6_new(ctx: Context): Promise<Serializable> {
return this.acRelayCommon(ctx);
}
async sv6_load(ctx: Context): Promise<Serializable> {
// hack for maomani
// chou xiang amao
ctx.acRelayInfo.request['cardid'] = v.str(tokenToCardNumber(ctx.token));
return this.acRelayCommon(ctx);
}
@generic()
+92 -3
View File
@@ -2,12 +2,19 @@ import { Combine } from "../../utils/combine.js";
import { AcRelay } from "./ac-relay.js";
import { UserService } from "../../services/sdvx/user.js";
import { inject, singleton } from "tsyringe";
import { User } from "./user.js";
import { Music } from "./music.js";
import { EacGeneric } from "../eac-generic.js";
import { generic } from "../../decorators/eacnet.js";
import { Context } from "../../types.js";
import { Serializable, v } from "../../utils/kxml-value.js";
import { requestEa3Typed } from '../../utils/ea3.js';
import { tokenToCardNumber, tokenToSnsId } from "../../utils/laochan-id.js";
import * as data from '../../datas/sdvx.js';
export const SDVX_AC_MODEL = 'KFC:A:B:C:20250414';
@singleton()
export default class extends Combine(AcRelay, User, Music, EacGeneric) {
export default class extends Combine(AcRelay, EacGeneric) {
constructor(
@inject(UserService) private readonly userService: UserService,
) {
@@ -15,4 +22,86 @@ export default class extends Combine(AcRelay, User, Music, EacGeneric) {
this.userService;
}
@generic()
async getItemList(): Promise<Serializable> {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
item_num: v.s32(data.itemList.length),
item: data.itemList.map(id => ({
item_id: v.str(id),
not_free_count: v.s32(1),
free_count: v.s32(0),
})),
}
};
}
@generic()
async getUserIDs(ctx: Context): Promise<Serializable> {
const card = tokenToCardNumber(ctx.token);
const { cardmng: inquire } = await requestEa3Typed<{
cardmng: {
status: string;
dataid: string;
refid: string;
}
}>({
module: 'cardmng',
method: 'inquire',
request: {
$cardtype: '4',
$update: '0'
}
}, SDVX_AC_MODEL, ctx.token);
if (inquire.status == '0') {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
card_num: v.str(card),
ref_id: v.str(inquire.refid),
data_id: v.str(inquire.dataid),
sns_id: v.str(tokenToSnsId(ctx.token)),
},
};
}
const { cardmng: getrefid } = await requestEa3Typed<{
cardmng: {
status: string;
dataid: string;
refid: string;
}
}>({
module: 'cardmng',
method: 'getrefid',
request: {
// TODO: use hash from token instead of 0000?
$passwd: '0000',
}
}, SDVX_AC_MODEL, ctx.token);
if (getrefid.status !== '0') {
return {
status: v.s32(+getrefid.status),
error_code: v.s32(-1),
}
}
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
card_num: v.str(card),
ref_id: v.str(getrefid.refid),
data_id: v.str(getrefid.dataid),
sns_id: v.str(tokenToSnsId(ctx.token)),
},
};
}
}
-126
View File
@@ -1,126 +0,0 @@
import { generic } from "../../decorators/eacnet.js";
import { UserService } from "../../services/sdvx/user.js";
import { Serializable, v } from "../../utils/kxml-value.js";
import { Context } from "../../types.js";
import { PlayerMusicData, PlayerPlayLog } from "../../types/sdvx/index.js";
export class Music {
userService: UserService;
@generic()
async sv6_save_m(ctx: Context): Promise<Serializable> {
const { track } = ctx.body as { track: PlayerPlayLog };
track.player = ctx.token;
track.play_time = new Date();
const mdata = (await this.userService.getPlayerMusicData(ctx.token, track.music_id, track.music_type)) ?? {
player: ctx.token,
music_id: track.music_id,
music_type: track.music_type,
score: 0, exscore: 0, clear_type: 0, score_grade: 0,
max_chain: 0, play_count: 0,
btn_rate: 0, long_rate: 0, vol_rate: 0,
best_play_time: track.play_time,
} as PlayerMusicData;
if (track.score > mdata.score) {
mdata.score = track.score;
mdata.score_grade = track.score_grade;
mdata.best_play_time = track.play_time;
mdata.btn_rate = track.btn_rate;
mdata.long_rate = track.long_rate;
mdata.vol_rate = track.vol_rate;
}
if (track.exscore > mdata.score) {
mdata.exscore = track.exscore;
// if score is same, use exscore to compare
if (track.score == mdata.score) {
mdata.best_play_time = track.play_time;
mdata.btn_rate = track.btn_rate;
mdata.long_rate = track.long_rate;
mdata.vol_rate = track.vol_rate;
}
}
if (track.clear_type > mdata.clear_type) {
mdata.clear_type = track.clear_type;
}
if (track.max_chain > mdata.max_chain) {
mdata.max_chain = track.max_chain;
}
mdata.play_count++;
await Promise.all([
this.userService.insertPlayLog(track),
this.userService.upsertMusicData(mdata),
]);
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: {
$status: 0,
}
}
};
}
@generic()
async sv6_load_r(): 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: {
game: {
$status: 0,
}
}
};
}
/*
* music_id diff
* score exscore clear_type score_grade max_chain play_count btn_rate long_rate vol_rate
* 0 0 0 0 0 timestamp_s 0 0 0 0 0 0
*/
@generic()
async sv6_load_m(ctx: Context): Promise<Serializable> {
const mdata = await this.userService.getPlayerMusicDatas(ctx.token);
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: {
$status: 0,
music: {
info: mdata.map(d => ({
date: d.best_play_time,
param: v.u32([
d.music_id, d.music_type,
d.score, d.exscore, d.clear_type, d.score_grade, d.max_chain,
d.play_count, d.btn_rate, d.long_rate, d.vol_rate,
0, 0, 0, 0, 0,
Math.floor(d.best_play_time.valueOf() / 1000),
0, 0, 0, 0, 0, 0,
])
}))
},
}
}
};
}
}
-244
View File
@@ -1,244 +0,0 @@
import * as data from '../../datas/sdvx.js';
import { Context } from "../../types.js";
import { generic } from '../../decorators/eacnet.js';
import { KValueG, Serializable, ValueTypes, v } from '../../utils/kxml-value.js';
import { UserService } from '../../services/sdvx/user.js';
import { Item, Param } from '../../types/sdvx/savedata.js';
import { dateToString } from '../../utils/time.js';
function trySet<T>(kvalue: KValueG<ValueTypes, T>, value: T) {
if (value === undefined || value === null)
return;
kvalue.__value = value;
}
function tryAdd(kvalue: KValueG<ValueTypes, number>, value: number) {
if (value === undefined || value === null)
return;
if (kvalue.__value instanceof Array)
return;
kvalue.__value += value;
}
export function tryMergeItem(params: Item[], newParams: object[]) {
if (!params) {
params = [];
}
if (!newParams) {
return params;
}
if (!(newParams instanceof Array)) {
newParams = [newParams];
}
for (const np of newParams) {
const target = params.find(p => p.id.__value === np['id'] && p.type.__value === np['type']);
if (target) {
target.type = v.u8(np['type']);
target.param = v.u32(np['param']);
continue;
}
params.push({
type: v.u8(np['type']),
id: v.u32(np['id']),
param: v.u32(np['param']),
})
}
return params;
}
export function tryMergeParam(params: Param[], newParams: object[]) {
if (!params) {
params = [];
}
if (!newParams) {
return params;
}
if (!(newParams instanceof Array)) {
newParams = [newParams];
}
for (const np of newParams) {
const target = params.find(p => p.id.__value === np['id'] && p.type.__value === np['type']);
if (target) {
target.param = v.s32(np['param']);
continue;
}
params.push({
type: v.s32(np['type']),
id: v.s32(np['id']),
param: v.s32(np['param']),
})
}
return params;
}
export class User {
userService: UserService;
@generic()
async sv6_save_c(): 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: {
game: {
$status: 0,
play_id: v.u32(Math.floor(new Date().valueOf() / 6e4)),
},
}
};
}
@generic()
async sv6_save(ctx: Context): Promise<Serializable> {
const playData = await this.userService.getPlayerData(ctx.token);
if (!playData) {
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(1),
xrpc_fault_code: v.s32(404),
};
}
const newData = ctx.body;
trySet(playData.appeal_id, newData.appeal_id);
trySet(playData.skill_level, newData.skill_level);
trySet(playData.skill_base_id, newData.skill_base_id);
trySet(playData.skill_name_id, newData.skill_name_id);
tryAdd(playData.blaster_energy, newData.earned_blaster_energy);
tryAdd(playData.gamecoin_packet, newData.earned_gamecoin_packet);
tryAdd(playData.gamecoin_block, newData.earned_gamecoin_block);
tryAdd(playData.play_count, 1);
trySet(playData.hispeed, newData.hispeed);
trySet(playData.lanespeed, newData.lanespeed);
trySet(playData.gauge_option, newData.gauge_option);
trySet(playData.ars_option, newData.ars_option);
trySet(playData.notes_option, newData.notes_option);
trySet(playData.early_late_disp, newData.early_late_disp);
trySet(playData.draw_adjust, newData.draw_adjust);
trySet(playData.eff_c_left, newData.eff_c_left);
trySet(playData.eff_c_right, newData.eff_c_right);
trySet(playData.last_music_id, newData.music_id);
trySet(playData.last_music_type, newData.music_type);
trySet(playData.sort_type, newData.sort_type);
trySet(playData.narrow_down, newData.narrow_down);
trySet(playData.headphone, newData.headphone);
playData.item.info = tryMergeItem(playData.item.info, newData.item?.info);
playData.item_infinite.info = tryMergeItem(playData.item_infinite.info, newData.item_infinite?.info);
playData.param.info = tryMergeParam(playData.param.info, newData.param?.info);
playData.last_date = v.str(dateToString(new Date()));
if (!await this.userService.savePlayerData(ctx.token, playData)) {
ctx.logger.error('play data save failed');
}
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: { $status: 0 },
}
};
}
@generic()
async sv6_new(ctx: Context): Promise<Serializable> {
await this.userService.createEmptyPlayerData(ctx.token, ctx.body.name);
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: {
$status: 0,
}
}
};
}
@generic()
async sv6_load(ctx: Context): Promise<Serializable> {
const playData = await this.userService.getPlayerData(ctx.token);
if (!playData) {
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: {
$status: 0,
result: v.u8(1),
}
}
}
}
// blaster pass
playData.ea_shop.blaster_pass_enable = v.bool(true);
playData.ea_shop.blaster_pass_limit_date = v.u64(new Date().valueOf() + (30 * 60 * 60 * 1000));
// unlock all navigaters
playData.item.info = tryMergeItem(playData.item.info, data.charaItems);
playData.start_date = v.str(dateToString(new Date()));
return {
status: v.s32(0),
error_code: v.s32(0),
xrpc_status_code: v.s32(0),
xrpc_fault_code: v.s32(0),
response: {
game: {
$status: 0,
result: v.u8(0),
...playData,
}
}
};
}
@generic()
async getItemList(): Promise<Serializable> {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
item_num: v.s32(data.itemList.length),
item: data.itemList.map(id => ({
item_id: v.str(id),
not_free_count: v.s32(1),
free_count: v.s32(0),
})),
}
};
}
}
-2265
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
import NodeCache from 'node-cache';
const nodeCache = new NodeCache({
stdTTL: 60 * 60 * 12,
});
export function cache(cacheKey: string, ttl?: number) {
return function(_target: any, _propertyKey: string, descriptor: PropertyDescriptor) {
const orig = descriptor.value as Function;
descriptor.value = async function(...args: any[]) {
const cacheResult = nodeCache.get<unknown>(cacheKey);
if (cacheResult) {
return cacheResult;
}
const result = await orig.apply(this, args);
nodeCache.set<unknown>(cacheKey, result, ttl);
return result;
} as unknown as any;
};
}
+9 -3
View File
@@ -1,7 +1,7 @@
import { Next } from 'koa';
import { Context } from '../types.js';
import { LZ77 } from '../utils/lz77.js';
import { fromKBinXml } from '../utils/kbinxml.js';
import { fromKBinXml, toObject } from '../utils/kbinxml.js';
export async function eacnet(ctx: Context, next: Next): Promise<any> {
const body = ctx.request.body as {
@@ -27,7 +27,8 @@ export async function eacnet(ctx: Context, next: Next): Promise<any> {
);
const decoded = LZ77.decompress(buffer);
const result = fromKBinXml(decoded);
const xml = fromKBinXml(decoded);
const result = toObject(xml);
if (result['eacnet']) {
const info = result['eacnet'].info;
@@ -45,7 +46,12 @@ export async function eacnet(ctx: Context, next: Next): Promise<any> {
};
ctx.body = request.data ?? {};
ctx.eacnetRequest = request.service ? request : undefined;
ctx.acRelayInfo = request.service ? {
module: request.module,
method: request.method,
request: xml['eacnet']['request']['data'],
} : undefined;
return next();
}
+8 -6
View File
@@ -1,6 +1,13 @@
import { Logger } from '@cordisjs/logger';
import { DefaultState, ParameterizedContext } from 'koa';
import { InjectionToken } from 'tsyringe';
import { Serializable } from './utils/kxml-value.js';
export interface AcRelayInfo {
module: string;
method: string;
request: Serializable;
}
export interface ILaochanContext {
token?: string;
@@ -11,12 +18,7 @@ export interface ILaochanContext {
method: string,
};
eacnetRequest?: {
service: string;
module: string;
method: string;
data: unknown;
};
acRelayInfo?: AcRelayInfo;
logger: Logger;
body?: unknown;
+61
View File
@@ -0,0 +1,61 @@
import { AcRelayInfo } from '../types.js';
import { fromKBinXml, toKBinXml, toObject } from './kbinxml.js';
import { Serializable } from './kxml-value.js';
import { LZ77 } from './lz77.js';
import { tokenToCardNumber } from './laochan-id.js';
import config from './config.js';
const PCB_ID = '1A0C1A0C1A0C1A0C1A0C';
export async function requestEa3(info: AcRelayInfo, model: string, token: string, ea3Url: string = 'http://maomani.cn:573/'): Promise<{
status: number,
response: Serializable,
}> {
if (config.isDev) {
console.log('ea3 call:');
}
const request = toKBinXml('call', {
[info.module]: {
$method: info.method,
$model: model,
$cardid: tokenToCardNumber(token),
$srcid: PCB_ID,
...info.request,
},
}, 'UTF-8', config.isDev);
const compressed = LZ77.compress(request.data);
const body = new Blob([compressed]);
const result = await fetch(`${ea3Url}/?model=${model}&f=${info.module}.${info.method}`, {
method: 'post',
body,
headers: {
'X-Compress': 'lz77',
},
}).then(async r => {
const raw = new Uint8Array(await r.arrayBuffer());
if (r.headers.get('x-compress') === 'lz77') {
return new Uint8Array(LZ77.decompress(raw));
}
return raw;
});
if (config.isDev) {
console.log('ea3 response:');
}
const response = fromKBinXml(result, config.isDev);
return {
status: parseInt(response['response'][info.module]?.$status ?? '0'),
response: response['response'],
};
}
export async function requestEa3Typed<T>(info: AcRelayInfo, model: string, token: string, ea3Url?: string): Promise<T> {
const { response } = await requestEa3(info, model, token, ea3Url);
return toObject<T>(response);
}
+29 -20
View File
@@ -1,4 +1,4 @@
import { to_bin, to_xml } from "@kamyu/kbinxml";
import { to_bin, to_xml } from "@geekidos/kbinxml";
import { XMLParser } from "fast-xml-parser";
import _ from "lodash";
import { Serializable } from "./kxml-value.js";
@@ -8,6 +8,7 @@ export const parser = new XMLParser({
parseAttributeValue: false,
attributeNamePrefix: '$',
removeNSPrefix: true,
textNodeName: '__value',
numberParseOptions: {
hex: false,
leadingZeros: false,
@@ -32,49 +33,49 @@ function parseValue(node: { $__type: string; $__count?: unknown }): any {
].includes(node.$__type)
) {
if (isArray) {
return node['#text'].split(' ').map((v) => parseInt(v));
return node['__value'].split(' ').map((v) => parseInt(v));
}
return parseInt(node['#text']);
return parseInt(node['__value']);
}
if (['float', 'double'].includes(node.$__type)) {
if (isArray) {
return node['#text'].split(' ').map((v) => parseFloat(v));
return node['__value'].split(' ').map((v) => parseFloat(v));
}
return parseFloat(node['#text']);
return parseFloat(node['__value']);
}
if (['b', 'bool'].includes(node.$__type)) {
return !!parseInt(node['#text']);
return !!parseInt(node['__value']);
}
if (['bin', 'binary'].includes(node.$__type)) {
return Buffer.from(node['#text'], 'hex');
return Buffer.from(node['__value'], 'hex');
}
if (['ip4', 'str', 'string'].includes(node.$__type)) {
return node['#text'];
return node['__value'];
}
if (node.$__type == 'time') {
return new Date(parseInt(node['#text']) * 1000);
return new Date(parseInt(node['__value']) * 1000);
}
return {
type: node.$__type,
value: node['#text'],
value: node['__value'],
};
}
export function toObject(node: Record<string, unknown> | unknown[] | string): any {
export function toObject<T = any>(node: Record<string, unknown> | unknown[] | string): T {
if (node instanceof Array) {
return node.map((v) => toObject(v as Record<string, unknown>));
return node.map((v) => toObject(v as Record<string, unknown>)) as T;
}
if (typeof node === 'string') {
return {};
return {} as T;
}
if ('$__type' in node) {
@@ -95,13 +96,17 @@ export function toObject(node: Record<string, unknown> | unknown[] | string): an
obj[key] = toObject(node[key] as Record<string, unknown>);
}
return obj;
return obj as T;
}
export function fromKBinXml(kbinxml: Uint8Array) {
const xml = to_xml(kbinxml).data;
const parsedXml = parser.parse(xml);
return toObject(parsedXml);
export function fromKBinXml(kbinxml: Uint8Array, dumpXml: boolean = false) {
const xml = to_xml(kbinxml, true).data;
if (dumpXml) {
console.log(xml);
}
return parser.parse(xml);
}
function serializeValue(value: any, type: string): string {
@@ -239,8 +244,12 @@ function serializeObject(obj: Serializable, name: string, linePrefix: string = '
return output + `</${name}>\n`;
}
export function toKBinXml(topName: string, obj: Serializable, encoding: 'UTF-8' | 'SHIFT_JIS' = 'UTF-8') {
export function toKBinXml(topName: string, obj: Serializable, encoding: 'UTF-8' | 'SHIFT_JIS' = 'UTF-8', dumpXml: boolean = false) {
const xml = `<?xml version="1.0" encoding="${encoding}"?>` + serializeObject(obj, topName);
console.log(xml);
if (dumpXml) {
console.log(xml);
}
return to_bin(xml);
}
+5 -58
View File
@@ -1,64 +1,11 @@
import {compress, decompress} from '@kamyu/lz77';
export class LZ77 {
static compress(input: ArrayLike<number>) {
const a = Uint8Array.from(input);
const res = new Uint8Array(a.length + a.length / 8 + 3);
let p = 0;
for (let i = 0; i < a.length; ++i) {
if (i % 8 === 0) {
if (a.length - i < 8) {
res[p] = (Math.pow(2, a.length - i) - 1);
} else {
res[p] = 255;
}
p++;
}
res[p] = a[i];
p++;
}
res[p] = 0;
res[p + 1] = 0;
return Buffer.from(res);
return compress(Uint8Array.from(input));
}
static decompress(a: ArrayLike<number>) {
const res = new Uint8Array(0x190000);
let p = 0;
let r = 0;
let t = 0;
let b = 8;
let mask = 0;
while (true) {
if (b === 8) {
mask = a[p];
p++;
b = 0;
}
if ((mask & 1) === 1) {
res[r] = a[p];
r++;
p++;
} else {
let distance = a[p];
let count = a[p + 1];
if (distance === 0 && count === 0) break;
p += 2;
distance <<= 4;
distance |= count >> 4;
count = (count & 0x0F) + 3;
t = r - distance;
for (let i = 0; i < count; ++i) {
res[r] = t < 0 ? 0x00 : res[t];
r++;
t++;
}
}
mask >>= 1;
b += 1;
}
const o = res.slice(0, r);
return Buffer.from(o);
static decompress(input: ArrayLike<number>) {
return decompress(Uint8Array.from(input));
}
}