mirror of
https://github.com/laochan-eacnet/backend.git
synced 2026-09-22 22:28:11 +03:00
pass sdvx requests to maomani
This commit is contained in:
Generated
+350
-385
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
}))
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user