mirror of
https://github.com/laochan-eacnet/backend.git
synced 2026-09-22 22:28:11 +03:00
ddr working now
This commit is contained in:
+2
-2
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
},
|
||||
|
||||
],
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('')
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -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,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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 '<';
|
||||
case '>': return '>';
|
||||
case '&': return '&';
|
||||
case '\'': return ''';
|
||||
case '"': return '"';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user