working sdvx trial mode

This commit is contained in:
GEEKiDoS
2024-04-18 02:36:12 +08:00
parent 6859700630
commit 67edcba7ba
17 changed files with 16408 additions and 7808 deletions
+38 -12
View File
@@ -1,22 +1,48 @@
import { Context } from '../types.js';
import { eacnet } from '../decorators/eacnet.js';
import { p2d, sdvx } from '../decorators/eacnet.js';
import { v } from '../utils/kxml-value.js';
import { singleton } from 'tsyringe';
@singleton()
export class DefaultService {
async default(ctx: Context) {
const descriptor = {
value: () => ({
status: v.s32(0),
error: v.s32(0),
result: {
dummy: v.u8(0),
}
})
};
if (ctx.service.name === 'p2d') {
const descriptor = {
value: () => ({
status: v.s32(0),
error: v.s32(0),
result: {},
})
};
eacnet(ctx.service.name)(undefined, undefined, descriptor);
return descriptor.value();
p2d()(undefined, undefined, descriptor);
return descriptor.value();
} else if (ctx.service.name === 'sdvx') {
const result = {
status: v.s32(0),
error_code: v.s32(0),
response: {},
};
// handle ac relay
if (ctx.eacnetRequest) {
result['xrpc_status_code'] = v.s32(0);
result['xrpc_fault_code'] = v.s32(0);
result['response'] = {
[ctx.eacnetRequest.module]: {
$status: 0,
}
}
}
const descriptor = {
value: () => result,
};
sdvx()(undefined, undefined, descriptor);
return descriptor.value();
}
throw new Error(`unsupported game ${ctx.service.name}`);
}
}
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,9 +1,9 @@
import { eacnet } from "../../decorators/eacnet.js";
import { p2d } from "../../decorators/eacnet.js";
import { Context } from "../../types.js";
import { v } from "../../utils/kxml-value.js";
export class Game {
@eacnet('p2d')
@p2d()
async getChampionshipInfo() {
return {
status: v.s32(0),
@@ -23,7 +23,7 @@ export class Game {
}
}
@eacnet('p2d')
@p2d()
async getServerValues() {
return {
status: v.s32(0),
@@ -41,7 +41,7 @@ export class Game {
}
}
@eacnet('p2d')
@p2d()
async checkGameVersion() {
return {
status: v.s32(0),
@@ -52,7 +52,7 @@ export class Game {
};
}
@eacnet('p2d')
@p2d()
async getClearRate(ctx: Context) {
const { play_style } = ctx.body as { play_style: number };
return {
@@ -63,7 +63,7 @@ export class Game {
}
}
@eacnet('p2d')
@p2d()
async getGoodsList() {
const goods = [{
goods_id: v.str('G1000000'),
+2 -23
View File
@@ -2,32 +2,11 @@ import { v } from "../../utils/kxml-value.js";
import { Game } from "./game.js";
import { Service } from "./service.js";
import { User } from "./user.js";
import data from './data.json' with { type: "json" };
import { inject, singleton } from "tsyringe";
import { UserService } from "../../services/p2d/user.js";
import { Music } from "./music.js";
function Combine(...modules: ({ new(): any })[]) {
class Modules {
constructor() {
modules.forEach(v => Object.assign(this, new v()));
}
}
modules.forEach(v => {
const descriptors = Object.getOwnPropertyDescriptors(v.prototype);
for (const name in descriptors) {
if (name === 'constructor')
continue;
const descriptor = descriptors[name];
Object.defineProperty(Modules.prototype, name, descriptor);
}
});
return Modules;
}
import { Combine } from "../../utils/combine.js";
import * as data from '../../datas/p2d.js'
@singleton()
export default class extends Combine(Service, User, Game, Music) {
+7 -7
View File
@@ -1,11 +1,11 @@
import { PlayerPlayLog } from "../../types/index.js";
import { eacnet } from "../../decorators/eacnet.js";
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 data from './data.json' with { type: "json" };
import * as data from '../../datas/p2d.js'
// user only needs to unlock omni music, which is temporary
const userUnlockMusic = new Map<string, number>();
@@ -13,7 +13,7 @@ const userUnlockMusic = new Map<string, number>();
export class Music {
userService: UserService;
@eacnet('p2d')
@p2d()
async getMusicGhost(ctx: Context) {
// playstyle is actually chart diff, game is sending wrong request
// TODO: check game code and maybe patch it later
@@ -85,7 +85,7 @@ export class Music {
};
}
@eacnet('p2d')
@p2d()
async reportMusicResult(ctx: Context) {
const result = ctx.body as PlayerPlayLog;
result.player = ctx.token;
@@ -131,7 +131,7 @@ export class Music {
};
}
@eacnet('p2d')
@p2d()
async unlockMusic(ctx: Context) {
const { music: { music_id } } = ctx.body as { music: { music_id: number } };
userUnlockMusic.set(ctx.token, music_id);
@@ -144,7 +144,7 @@ export class Music {
};
}
@eacnet('p2d')
@p2d()
async getMusicList(ctx: Context) {
let music = [...MUSIC_LIST];
@@ -177,7 +177,7 @@ export class Music {
};
}
@eacnet('p2d')
@p2d()
async getMusicData(ctx: Context) {
const { play_style } = ctx.body as { play_style: number };
+7 -7
View File
@@ -1,10 +1,10 @@
import { eacnet } from '../../decorators/eacnet.js';
import { p2d } from '../../decorators/eacnet.js';
import { Context } from '../../types.js';
import config from '../../utils/config.js';
import { v } from '../../utils/kxml-value.js';
export class Service {
@eacnet('p2d')
@p2d()
async sendLog() {
return {
status: v.s32(0),
@@ -12,7 +12,7 @@ export class Service {
};
}
@eacnet('p2d')
@p2d()
async getServerState() {
return {
status: v.s32(0),
@@ -25,7 +25,7 @@ export class Service {
}
}
@eacnet('p2d')
@p2d()
async getServerClock() {
return {
status: v.s32(0),
@@ -36,7 +36,7 @@ export class Service {
}
}
@eacnet('p2d')
@p2d()
async heartbeat(ctx: Context) {
return {
status: v.s32(0),
@@ -51,7 +51,7 @@ export class Service {
};
}
@eacnet('p2d')
@p2d()
async checkSendLogAvailable() {
return {
status: v.s32(0),
@@ -63,7 +63,7 @@ export class Service {
};
}
@eacnet('p2d')
@p2d()
async getServices() {
return {
status: v.s32(0),
+18 -18
View File
@@ -1,4 +1,4 @@
import { eacnet } from "../../decorators/eacnet.js";
import { p2d } from "../../decorators/eacnet.js";
import { Context } from "../../types.js";
import { v } from "../../utils/kxml-value.js";
import { fromToken } from "../../utils/laochan-id.js";
@@ -10,7 +10,7 @@ import { ITEM_LIST } from "./index.js";
export class User {
userService: UserService;
@eacnet('p2d')
@p2d()
async addPoint() {
return {
status: v.s32(0),
@@ -18,7 +18,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async reserveChangePoint() {
return {
status: v.s32(0),
@@ -26,7 +26,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async sendGradeCertificationLog(ctx: Context) {
await this.userService.addCourseLog({
player: ctx.token,
@@ -39,7 +39,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async consumeItem(ctx: Context) {
const { item_id } = ctx.body;
const { items_count } = await this.userService.getCustomizeSetting(ctx.token);
@@ -75,7 +75,7 @@ export class User {
};
}
@eacnet('p2d')
@p2d()
async reserveConsumeItem() {
return {
status: v.s32(0),
@@ -83,7 +83,7 @@ export class User {
};
}
@eacnet('p2d')
@p2d()
async getCustomizeSetting(ctx: Context) {
const { customize, other_customize } = await this.userService.getCustomizeSetting(ctx.token);
@@ -103,7 +103,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async gameEnd() {
return {
status: v.s32(0),
@@ -111,7 +111,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async getPointList(ctx: Context) {
const customize = await this.userService.getCustomizeSetting(ctx.token);
@@ -128,7 +128,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async getPrivilegeClient() {
return {
status: v.s32(0),
@@ -139,7 +139,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async getPrivilegeServer() {
return {
status: v.s32(0),
@@ -150,7 +150,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async savePlayData(ctx: Context) {
const { pdata, check_sum } = ctx.body as { pdata: Buffer, check_sum: string };
const localChecksum = sha256(pdata);
@@ -167,7 +167,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async getPlayData(ctx: Context) {
const result = await this.userService.getPDataBinary(ctx.token);
@@ -190,7 +190,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async registPlayer(ctx: Context) {
const { pdata, check_sum } = ctx.body as { pdata: Buffer, check_sum: string };
const localChecksum = sha256(pdata);
@@ -207,7 +207,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async checkPlayData(ctx: Context) {
const { check_sum } = ctx.body as { check_sum: string };
@@ -249,7 +249,7 @@ export class User {
};
}
@eacnet('p2d')
@p2d()
async getRivalInfo(ctx: Context) {
const rivalData = await this.userService.getPlayerRivalData(ctx.token);
if (!rivalData.enabled) {
@@ -320,7 +320,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async getCompeScoreData() {
return {
status: v.s32(0),
@@ -329,7 +329,7 @@ export class User {
}
}
@eacnet('p2d')
@p2d()
async getItemList(ctx: Context) {
const { items_count } = await this.userService.getCustomizeSetting(ctx.token);
+40
View File
@@ -0,0 +1,40 @@
import { sdvx } from "../../decorators/eacnet.js";
import { Context } from "../../types.js";
import { v } from "../../utils/kxml-value.js";
import * as data from '../../datas/sdvx.js';
export class AcRelay {
@sdvx()
async sv6_music_url(ctx: Context) {
let { music_id } = ctx.body as { music_id: number | number[] };
if (typeof music_id === 'number') {
music_id = [music_id];
}
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: music_id.map(id => {
const music = data.musicDownloadLinks[id];
if (!music) {
return {};
}
return {
music_id: v.s32(id),
url: v.str(music.url),
hash: v.str(music.hash),
size: v.s32(music.size),
}
}),
}
}
};
}
}
+194
View File
@@ -0,0 +1,194 @@
import { sdvx } from "../../decorators/eacnet.js";
import { Combine } from "../../utils/combine.js";
import config from "../../utils/config.js";
import { v } from "../../utils/kxml-value.js";
import { AcRelay } from "./ac-relay.js";
export default class extends Combine(AcRelay) {
@sdvx()
async getItemList() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
item_num: v.s32(0),
}
};
}
@sdvx()
async getGoodsList() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
goods_num: v.s32(0),
}
};
}
@sdvx()
async getUserIDs() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
card_num: v.str('0123456789ABCDEF'),
ref_id: v.str('0123456789ABCDEF'),
data_id: v.str('0123456789ABCDEF'),
sns_id: v.str('012345678'),
}
};
}
@sdvx()
async getSubscriptionStatus() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
subscription: {
name: v.str('test'),
},
}
};
}
@sdvx()
async heartbeat() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
state: v.s32(1),
next_clock: v.u64(new Date().valueOf() + 36e5),
server_state: v.s32(1),
mainte_start_clock: v.u64(0),
mainte_end_clock: v.u64(0),
usta_boot_status: v.s32(1),
usta_time_remain: v.s32(0),
}
};
}
@sdvx()
async checkVersion() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
permission: v.s32(1),
}
};
}
@sdvx()
async getServerClock() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
server_clock: v.u64(new Date().valueOf()),
}
}
}
@sdvx()
async getServerState() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
server_state: v.s32(1),
mainte_start_clock: v.u64(0),
mainte_end_clock: v.u64(0),
}
}
}
@sdvx('SHIFT_JIS')
async getServices() {
return {
status: v.s32(0),
error_code: v.s32(0),
response: {
service_num: v.s32(18),
service: [
{
service_name: v.str('acRelay'),
url: v.str(`${config.selfUrl}/need_auth/AcRelay`),
},
{
service_name: v.str('cancelReserveConsumeItem'),
url: v.str(`${config.selfUrl}/need_auth/CancelReserveConsumeItem`),
},
{
service_name: v.str('checkGameStart'),
url: v.str(`${config.selfUrl}/need_auth/CheckGameStart`),
},
{
service_name: v.str('checkVersion'),
url: v.str(`${config.selfUrl}/needless_auth/CheckVersion`),
},
{
service_name: v.str('consumeItem'),
url: v.str(`${config.selfUrl}/need_auth/ConsumeItem`),
},
{
service_name: v.str('getGoodsList'),
url: v.str(`${config.selfUrl}/needless_auth/GetGoodsList`),
},
{
service_name: v.str('getHash'),
url: v.str(`${config.selfUrl}/needless_auth/GetHash`),
},
{
service_name: v.str('getItemList'),
url: v.str(`${config.selfUrl}/need_auth/GetItemList`),
},
{
service_name: v.str('getLauncherData'),
url: v.str(`${config.selfUrl}/needless_auth/GetLauncherData`),
},
{
service_name: v.str('getResourceInfo'),
url: v.str(`${config.selfUrl}/needless_auth/GetResourceInfo`),
},
{
service_name: v.str('getServerClock'),
url: v.str(`${config.selfUrl}/pre_process/GetServerClock`),
},
{
service_name: v.str('getServerState'),
url: v.str(`${config.selfUrl}/pre_process/GetServerStatus`),
},
{
service_name: v.str('getServices'),
url: v.str(`${config.selfUrl}/pre_process/GetServices`),
},
{
service_name: v.str('getSubscriptionStatus'),
url: v.str(`${config.selfUrl}/need_auth/GetSubscriptionStatus`),
},
{
service_name: v.str('getUserIDs'),
url: v.str(`${config.selfUrl}/need_auth/GetUserId`),
},
{
service_name: v.str('heartbeat'),
url: v.str(`${config.selfUrl}/need_auth/Heartbeat`),
},
{
service_name: v.str('reserveConsumeItem'),
url: v.str(`${config.selfUrl}/need_auth/ReserveConsumeItem`),
},
{
service_name: v.str('uploadFile'),
url: v.str(`${config.selfUrl}/needless_auth/UploadFile`),
},
],
}
};
}
}
+24
View File
@@ -43,6 +43,30 @@ export async function initMongoDb() {
meta.version = 1;
}
if (meta.version < 2) {
const collections = await db.listCollections().toArray();
const tryRenameCollection = async (src: string, dest: string) => {
if (!collections.some(v => v.name === src)) {
return;
}
const col = db.collection(src);
await col.rename(dest);
};
await Promise.all([
tryRenameCollection('player_play_data', 'p2d_play_data'),
tryRenameCollection('player_music_data', 'p2d_music_data'),
tryRenameCollection('player_play_log', 'p2d_play_log'),
tryRenameCollection('player_course_log', 'p2d_course_log'),
tryRenameCollection('player_customize_setting', 'p2d_customize_setting'),
tryRenameCollection('player_rival_data', 'p2d_rival_data'),
])
logger.info('upgraded database to ver 2, renamed prefix player to p2d.');
meta.version = 2;
}
await metaCol.updateOne({}, { $set: meta }, { upsert: true });
return db;
}
+7712
View File
File diff suppressed because it is too large Load Diff
+8287
View File
File diff suppressed because it is too large Load Diff
+17 -12
View File
@@ -1,19 +1,24 @@
import { LZ77 } from '../utils/lz77.js';
import { toKBinXml } from '../utils/kbinxml.js';
export function eacnet(topName: string, encoding: 'UTF-8' | 'SHIFT_JIS' = 'UTF-8'): MethodDecorator {
return function (_target, _propertyKey, descriptor) {
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 + 0x2E);
export function eacnet(protocol_name: string, topName: string, dataOffset: number) {
return (encoding: 'UTF-8' | 'SHIFT_JIS' = 'UTF-8') => {
return function (_target, _propertyKey, descriptor) {
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);
result.write('P2D:2015091800', 0, 'ascii');
result.set(compressed, 0x2E);
result.write(protocol_name, 0, 'ascii');
result.set(compressed, dataOffset);
return result;
} as unknown as any;
return result;
} as unknown as any;
}
}
}
export const p2d = eacnet('P2D:2015091800', 'p2d', 0x2E);
export const sdvx = eacnet('2020090800', 'eacnet', 0x2A);
+22 -6
View File
@@ -6,16 +6,16 @@ import { fromKBinXml } from '../utils/kbinxml.js';
export async function eacnet(ctx: Context, next: Next): Promise<any> {
const body = ctx.request.body as {
request: string;
p2d_token: string;
p2d_token?: string;
} | undefined;
if (!body || !body.request) {
return next();
}
const { request, p2d_token: token } = body;
const { request } = body;
if (!request || !token) {
if (!request) {
return next();
}
@@ -29,12 +29,28 @@ export async function eacnet(ctx: Context, next: Next): Promise<any> {
const decoded = LZ77.decompress(buffer);
const result = fromKBinXml(decoded);
const game = Object.keys(result)[0];
if (result['eacnet']) {
const info = result['eacnet'].info;
ctx.token = info.token;
if (!(game in result)) {
const request = result['eacnet'].request;
if (!request) {
return next();
}
ctx.service = {
name: info.game_id,
method: request.method,
};
ctx.body = request.data ?? {};
ctx.eacnetRequest = request;
return next();
}
// for p2d
const game = Object.keys(result)[0];
if (result[game].params) {
ctx.body = result.p2d.params;
} else {
@@ -46,6 +62,6 @@ export async function eacnet(ctx: Context, next: Next): Promise<any> {
method: result[game].method,
}
ctx.token = token;
ctx.token = body.p2d_token;
return next();
}
+6 -6
View File
@@ -15,27 +15,27 @@ export class UserService {
) { }
get playDataCol() {
return this.db.collection<PlayerPlayData>('player_play_data');
return this.db.collection<PlayerPlayData>('p2d_play_data');
}
get musicDataCol() {
return this.db.collection<PlayerMusicData>('player_music_data');
return this.db.collection<PlayerMusicData>('p2d_music_data');
}
get playLogCol() {
return this.db.collection<PlayerPlayLog>('player_play_log');
return this.db.collection<PlayerPlayLog>('p2d_play_log');
}
get courseLogCol() {
return this.db.collection<PlayerCourseLog>('player_course_log');
return this.db.collection<PlayerCourseLog>('p2d_course_log');
}
get customizeSettingCol() {
return this.db.collection<PlayerCustomizeSetting>('player_customize_setting');
return this.db.collection<PlayerCustomizeSetting>('p2d_customize_setting');
}
get rivalDataCol() {
return this.db.collection<PlayerRivalData>('player_rival_data');
return this.db.collection<PlayerRivalData>('p2d_rival_data');
}
addCourseLog(coruseLog: PlayerCourseLog) {
+7
View File
@@ -10,6 +10,13 @@ export interface ILaochanContext {
method: string,
};
eacnetRequest?: {
service: string;
module: string;
method: string;
data: unknown;
};
logger: Logger;
body?: unknown;
resolve: <T>(resolveToken: InjectionToken<T>) => T;
+21
View File
@@ -0,0 +1,21 @@
export function Combine(...modules: ({ new(): any })[]) {
class Modules {
constructor() {
modules.forEach(v => Object.assign(this, new v()));
}
}
modules.forEach(v => {
const descriptors = Object.getOwnPropertyDescriptors(v.prototype);
for (const name in descriptors) {
if (name === 'constructor')
continue;
const descriptor = descriptors[name];
Object.defineProperty(Modules.prototype, name, descriptor);
}
});
return Modules;
}