Add rival, ghost, customize item counts, bug fix

This commit is contained in:
GEEKiDoS
2024-04-13 12:35:21 +08:00
parent 323a4a8fd1
commit cd19c26c8c
11 changed files with 524 additions and 36 deletions
+3 -1
View File
@@ -26,4 +26,6 @@ build/
.eslintcache
# Misc
.DS_Store
.DS_Store
.env
+144 -2
View File
@@ -2,7 +2,8 @@ import { Logger } from "@cordisjs/logger";
import Router from "@koa/router";
import { Context } from "../index.js";
import { UserService } from "../../services/p2d/user.js";
import { PlayerCustomizeSetting } from "../../database/index.js";
import { PlayerCustomizeSetting, PlayerRivalData } from "../../types/index.js";
import { RivalPatch, RivalPostOrDelete } from "../../types/api.js";
const logger = new Logger('p2d-api');
const router = new Router({
@@ -21,7 +22,7 @@ const router = new Router({
const pdata = await ctx.resolve(UserService).getPDataDecoded(token);
ctx.status = pdata ? 200 : 404;
ctx.body = pdata ? pdata : {};
ctx.body = pdata ? { pdata } : {};
})
.get('/customize/:token', async (ctx: Context) => {
@@ -41,6 +42,147 @@ const router = new Router({
};
})
.get('/playlog/:token', async (ctx: Context) => {
let { skip, limit } = ctx.query;
const { token } = ctx.params;
if (skip instanceof Array) {
[skip] = skip;
}
if (limit instanceof Array) {
[limit] = limit;
}
const result = await ctx.resolve(UserService)
.getPlayLogs(token, skip ? parseInt(skip) : undefined, limit ? parseInt(limit) : undefined);
ctx.status = 200;
ctx.body = {
playLogs: result,
};
})
.get('/rival/:token', async (ctx: Context) => {
const { token } = ctx.params;
const result = await ctx.resolve(UserService)
.getPlayerRivalData(token);
if (!result.enabled) {
ctx.status = 200;
ctx.body = {
enabled: false,
spRivals: [],
dpRivals: [],
};
return;
}
const getRivalPlayer = async (tk: string) => {
const pdata = await ctx.resolve(UserService)
.getPDataDecoded(tk);
return pdata.player;
};
const [spRivals, dpRivals] = await Promise.all([
await Promise.all(result.sp.map(getRivalPlayer)),
await Promise.all(result.dp.map(getRivalPlayer)),
]);
ctx.status = 200;
ctx.body = {
enabled: true,
spRivals, dpRivals,
};
})
.patch('/rival/:token', async (ctx: Context) => {
const { token } = ctx.params;
const { enabled } = ctx.request.body as RivalPatch;
await ctx.resolve(UserService)
.upsertPlayerRivalData({
_id: token,
enabled,
} as PlayerRivalData);
ctx.status = 200;
ctx.body = {
success: true,
};
})
.post('/rival/:token', async (ctx: Context) => {
const { token } = ctx.params;
const { infinitas_id, type } = ctx.request.body as RivalPostOrDelete;
if (!infinitas_id) {
ctx.status = 400;
ctx.body = { error: 'infinitas_id is undefined' };
return;
}
const [existing, { player: target }] = await Promise.all([
ctx.resolve(UserService).getPlayerRivalData(token),
ctx.resolve(UserService).findPlayerByInfasId(infinitas_id),
]);
if (!target) {
ctx.status = 404;
ctx.body = { error: 'rival target not found' };
return;
}
if (target == token) {
ctx.status = 400;
ctx.body = { error: 'ni rival ni zi ji?' };
return;
}
const targetArray = type ? existing.dp : existing.sp;
if (targetArray.includes(target)) {
ctx.status = 400;
ctx.body = { error: 'rival target already exists' };
return;
}
targetArray.push(target);
await ctx.resolve(UserService)
.upsertPlayerRivalData(existing);
ctx.status = 200;
ctx.body = {
success: true,
};
})
.delete('/rival/:token', async (ctx: Context) => {
const { token } = ctx.params;
const { infinitas_id, type } = ctx.request.body as RivalPostOrDelete;
const [existing, { player: target }] = await Promise.all([
ctx.resolve(UserService).getPlayerRivalData(token),
ctx.resolve(UserService).findPlayerByInfasId(infinitas_id),
]);
if (type) {
existing.dp = existing.dp.filter(v => v != target)
} else {
existing.sp = existing.sp.filter(v => v != target)
}
await ctx.resolve(UserService)
.upsertPlayerRivalData(existing);
ctx.status = 200;
ctx.body = {
success: true,
};
})
.put('/customize/:token', async (ctx: Context) => {
const { token } = ctx.params;
+80 -2
View File
@@ -1,13 +1,86 @@
import { PlayerPlayLog } from "../../database/index.js";
import { PlayerPlayLog } from "../../types/index.js";
import { eacnet } 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";
export class Music {
userService: UserService;
@eacnet('p2d')
async getMusicGhost(ctx: Context) {
// playstyle is actually chart diff, game is sending wrong request
// TODO: check game code and maybe patch it later
const data = ctx.body as {
music_id: number,
note_id: number,
playstyle: number,
request_ghost: {
kind: number,
pref_id: number,
grade_id: number,
infinitas_id?: string,
}[] | {
kind: number,
pref_id: number,
grade_id: number,
infinitas_id?: string,
};
};
const { music_id, note_id } = data;
const request_ghost = 'kind' in data.request_ghost ? [data.request_ghost] : data.request_ghost;
const play_style = Math.floor(note_id / 5);
const diff = note_id % 5;
const ghost = await Promise.all(request_ghost.map(async req => {
if (req.kind == 2) {
const { player, djname } = await this.userService.findPlayerByInfasId(req.infinitas_id);
if (!player) {
return undefined;
}
const mdata = await this.userService.getMusicData(player, music_id, play_style);
if (mdata.best_score_clock[diff] === -1) {
return undefined;
}
const playlog = await this.userService.getPlayLog(player, mdata.best_score_clock[diff]);
return {
kind: v.s32(2),
ex_score: v.s32(playlog.score),
exist: v.bool(true),
ghost_data: v.bin(Buffer.from(playlog.ghost.buffer)),
dj_name: v.str(djname),
best_option: {
valid: v.bool(1),
arrange_0: v.s32(playlog.arrange_0),
arrange_1: v.s32(playlog.arrange_1),
assist: v.s32(playlog.assist),
flip: v.s32(playlog.flip),
}
}
}
return undefined;
}));
return {
status: v.s32(0),
error: v.s32(0),
result: {
music_id: v.s32(music_id),
note_id: v.s32(note_id),
ghost: ghost.filter(v => v),
},
};
}
@eacnet('p2d')
async reportMusicResult(ctx: Context) {
const result = ctx.body as PlayerPlayLog;
@@ -27,8 +100,10 @@ export class Music {
if (result.miss_count < musicData.miss_count[diff] || musicData.miss_count[diff] == -1)
musicData.miss_count[diff] = result.miss_count;
if (result.score > musicData.score[diff])
if (result.score > musicData.score[diff]) {
musicData.score[diff] = result.score;
musicData.best_score_clock[diff] = result.clock;
}
if (result.clear_flag >= 2)
musicData.clear_num[diff]++;
@@ -38,6 +113,9 @@ export class Music {
this.userService.upsertMusicData(musicData);
})();
// @ts-expect-error ghost is Buffer now, convert it to binary
result.ghost = new Binary(result.ghost);
await Promise.all([
this.userService.addPlayLog(result),
updateTask,
+122 -15
View File
@@ -1,9 +1,10 @@
import { eacnet } from "../../decorators/eacnet.js";
import { Context } from "../../types.js";
import { v } from "../../utils/kxml-value.js";
import { ITEM_LIST } from "./index.js";
import { fromToken } from "../../utils/laochan-id.js";
import { UserService } from "../../services/p2d/user.js";
import { sha256 } from "../../utils/sha256.js";
import { toKBinXml } from "../../utils/kbinxml.js";
export class User {
userService: UserService;
@@ -38,13 +39,37 @@ export class User {
}
@eacnet('p2d')
async consumeItem() {
async consumeItem(ctx: Context) {
const { item_id } = ctx.body;
const { items_count } = await this.userService.getCustomizeSetting(ctx.token);
let free_count = 0;
let not_free_count = 0;
if (item_id === 'I1000000') {
not_free_count = items_count.infinitas_ticket;
free_count = items_count.infinitas_ticket_free;
} else if (item_id === 'I1000001') {
not_free_count = items_count.ldisc;
free_count = 0;
}
free_count--;
if (free_count < 0) {
free_count = 0;
not_free_count--;
}
if (not_free_count < 0) {
not_free_count = 0;
}
return {
status: v.s32(0),
error: v.s32(0),
result: {
free_count: v.s32(1000),
not_free_count: v.s32(1000),
free_count: v.s32(free_count),
not_free_count: v.s32(not_free_count),
}
};
}
@@ -86,7 +111,9 @@ export class User {
}
@eacnet('p2d')
async getPointList() {
async getPointList(ctx: Context) {
const customize = await this.userService.getCustomizeSetting(ctx.token);
return {
status: v.s32(0),
error: v.s32(0),
@@ -94,7 +121,7 @@ export class User {
point_count: v.s32(1),
point: [{
point_id: v.str('P0100000'),
point_num: v.u32(114514),
point_num: v.u32(customize.items_count.bit),
}]
}
}
@@ -125,6 +152,11 @@ export class User {
@eacnet('p2d')
async savePlayData(ctx: Context) {
const { pdata, check_sum } = ctx.body as { pdata: Buffer, check_sum: string };
const localChecksum = sha256(pdata);
if (localChecksum != check_sum) {
ctx.logger.error('pdata checksum unmatch - excepted %s, actual %s', check_sum, localChecksum);
}
await this.userService.upsertPDataBinary(ctx.token, pdata, check_sum);
@@ -160,6 +192,11 @@ export class User {
@eacnet('p2d')
async registPlayer(ctx: Context) {
const { pdata, check_sum } = ctx.body as { pdata: Buffer, check_sum: string };
const localChecksum = sha256(pdata);
if (localChecksum != check_sum) {
ctx.logger.error('pdata checksum unmatch - excepted %s, actual %s', check_sum, localChecksum);
}
await this.userService.upsertPDataBinary(ctx.token, pdata, check_sum);
@@ -212,11 +249,73 @@ export class User {
}
@eacnet('p2d')
async getRivalInfo() {
async getRivalInfo(ctx: Context) {
const rivalData = await this.userService.getPlayerRivalData(ctx.token);
if (!rivalData.enabled) {
return {
status: v.s32(0),
error: v.s32(0),
result: {},
}
}
const getRivalDetail = async (id: string, playStyle: number) => {
const [pdata, musicDatas] = await Promise.all([
this.userService.getPDataDecoded(id),
this.userService.getMusicDatas(id, playStyle),
]);
return {
infinitas_id: v.str(pdata.player.infinitas_id),
dj_name: v.str(pdata.player.djname),
shop_name: v.str('Laochan Eacnet'),
pref_id: v.s32(pdata.player.pref_id),
grade_id_sp: v.s32(pdata.player.grade_id_sp),
grade_id_dp: v.s32(pdata.player.grade_id_dp),
music_data: {
music: musicDatas.map(data => ({
music_id: v.s32(data.music_id),
score: v.s32(data.score),
clear_flag: v.s32(data.clear_flag),
})),
},
challenges: {},
comment: {
status_comment: v.str(''),
rival_challenge_sweeping_victory: v.str(''),
rival_challenge_narrow_victory: v.str(''),
}
}
}
const [spRival, dpRival] = await Promise.all([
Promise.all(rivalData.sp.map(id => getRivalDetail(id, 0))),
Promise.all(rivalData.dp.map(id => getRivalDetail(id, 1))),
]);
const rivalInfo = {
sp: {
rival_num: v.s32(rivalData.sp.length),
rival: spRival,
},
dp: {
rival_num: v.s32(rivalData.dp.length),
rival: dpRival,
},
};
const rival_binary = Buffer.from(toKBinXml('rival_info', rivalInfo).data);
const rival_checksum = sha256(rival_binary);
return {
status: v.s32(0),
error: v.s32(0),
result: {},
result: {
check_sum: v.str(rival_checksum),
size: v.s32(rival_binary.length),
rival_data: v.bin(rival_binary),
rest_size: v.s32(0),
},
}
}
@@ -230,18 +329,26 @@ export class User {
}
@eacnet('p2d')
async getItemList() {
async getItemList(ctx: Context) {
const { items_count } = await this.userService.getCustomizeSetting(ctx.token);
return {
status: v.s32(0),
error: v.s32(0),
result: {
item_list: {
item_num: v.s32(ITEM_LIST.length),
item: ITEM_LIST.map(id => ({
item_id: v.str(id),
not_free_count: v.s32(1000),
free_count: v.s32(1000),
}))
item_num: v.s32(2),
item: [{
// infinitas ticket
item_id: v.str('I1000000'),
not_free_count: v.s32(items_count.infinitas_ticket),
free_count: v.s32(items_count.infinitas_ticket_free),
}, {
// ldisc
item_id: v.str('I1000001'),
not_free_count: v.s32(items_count.ldisc),
free_count: v.s32(0),
}]
}
}
};
+48
View File
@@ -0,0 +1,48 @@
import { Logger } from "@cordisjs/logger";
import { MongoClient } from "mongodb";
import config from "./utils/config.js";
import { PlayerPlayData } from "./types/index.js";
import { fromKBinXml } from "./utils/kbinxml.js";
import { Pdata } from "./types/pdata.js";
interface DatabaseMeta {
version: number;
}
export async function initMongoDb() {
const logger = new Logger('database');
logger.info('connecting to mongodb...');
const client = await MongoClient.connect(config.mongoUrl);
const db = client.db(config.dbName);
const metaCol = db.collection<DatabaseMeta>('__metadata');
let meta: DatabaseMeta = await metaCol.findOne({});
meta ??= { version: 0 };
// version 1: add infas id and djname to playdata collection
if (meta.version < 1) {
logger.info('upgrading database to ver 1');
const playDataCol = db.collection<PlayerPlayData>('player_play_data');
const cursor = playDataCol.find({});
const tasks = [];
for await (const player of cursor) {
const { pdata } = fromKBinXml(player.pdata.buffer) as { pdata: Pdata };
tasks.push(playDataCol.updateOne({ _id: player._id }, {
$set: {
djname: pdata.player.djname,
infinitas_id: pdata.player.infinitas_id,
}
}));
}
await Promise.all(tasks);
logger.info('upgraded database to ver 1, effected %d', tasks.length);
meta.version = 1;
}
await metaCol.updateOne({}, { $set: meta }, { upsert: true });
return db;
}
+5 -9
View File
@@ -10,8 +10,9 @@ import { DefaultService } from './controllers/default.js';
import { Logger } from '@cordisjs/logger';
import { eacnet } from './middlewares/eacnet.js';
import config from './utils/config.js';
import { MongoClient, Db } from 'mongodb';
import { Db } from 'mongodb';
import { apiRoutes } from './apis/index.js';
import { initMongoDb } from './database.js';
function tryResolve<T>(token: InjectionToken<T>): T | undefined {
if (!container.isRegistered(token)) return undefined;
@@ -34,14 +35,9 @@ async function main(): Promise<void> {
logger.info('initialization...');
{
logger.info('connecting to mongodb...');
const client = await MongoClient.connect(config.mongoUrl);
container.register(Db, {
useValue: client.db(config.dbName),
});
}
container.register(Db, {
useValue: await initMongoDb(),
});
logger.info('creating koa app...');
+92 -5
View File
@@ -1,7 +1,7 @@
import { Binary, Db } from "mongodb";
import { inject, injectable } from "tsyringe";
import { PlayerPlayData, PlayerMusicData, PlayerPlayLog, PlayerCourseLog, PlayerCustomizeSetting } from "../../database/index.js";
import { Pdata } from "../../database/pdata.js";
import { PlayerPlayData, PlayerMusicData, PlayerPlayLog, PlayerCourseLog, PlayerCustomizeSetting, PlayerRivalData } from "../../types/index.js";
import { Pdata } from "../../types/pdata.js";
import { fromKBinXml } from "../../utils/kbinxml.js";
@injectable()
@@ -34,6 +34,10 @@ export class UserService {
return this.db.collection<PlayerCustomizeSetting>('player_customize_setting');
}
get rivalDataCol() {
return this.db.collection<PlayerRivalData>('player_rival_data');
}
addCourseLog(coruseLog: PlayerCourseLog) {
return this.courseLogCol.insertOne(coruseLog);
}
@@ -42,6 +46,20 @@ export class UserService {
return this.playLogCol.insertOne(playLog);
}
getPlayLogs(player: string, skip = 0, limit = 10) {
return this.playLogCol.find({ player }, {
sort: { player: 1, clock: -1 },
skip, limit,
}).toArray();
}
getCourseLogs(player: string, skip = 0, limit = 10) {
return this.courseLogCol.find({ player }, {
sort: { player: 1, _id: -1 },
skip, limit,
}).toArray();
}
upsertCustomizeSetting(customizeSetting: PlayerCustomizeSetting) {
return this.customizeSettingCol.updateOne({ _id: customizeSetting._id }, {
$set: customizeSetting
@@ -50,11 +68,25 @@ export class UserService {
async getCustomizeSetting(player: string): Promise<PlayerCustomizeSetting> {
const result = await this.customizeSettingCol.findOne({ _id: player });
if (result)
if (result) {
result.items_count ??= {
bit: 15000,
ldisc: 5,
infinitas_ticket: 50,
infinitas_ticket_free: 9,
};
return result;
}
return {
_id: player,
items_count: {
bit: 15000,
ldisc: 5,
infinitas_ticket: 50,
infinitas_ticket_free: 9,
},
customize: [
{ item_category: 2, item_id: 'I1100000' },
{ item_category: 3, item_id: 'I1200000' },
@@ -90,6 +122,19 @@ export class UserService {
return undefined;
}
async findPlayerByInfasId(infinitas_id: string): Promise<{ player: string, djname: string } | undefined> {
const result = await this.playDataCol.findOne({ infinitas_id }, {
projection: {
_id: 1, djname: 1,
}
});
if (result)
return { player: result._id, djname: result.djname };
return undefined;
}
async getPDataBinary(player: string): Promise<{ pdata: Buffer, check_sum: string } | undefined> {
const result = await this.playDataCol.findOne({ _id: player });
@@ -106,13 +151,18 @@ export class UserService {
const binary = await this.getPDataBinary(player);
if (!binary) return undefined;
return fromKBinXml(binary.pdata);
return fromKBinXml(binary.pdata).pdata;
}
upsertPDataBinary(player: string, pdata: Buffer, check_sum: string) {
const unpacked = fromKBinXml(pdata) as { pdata: Pdata };
const { djname, infinitas_id } = unpacked.pdata.player;
return this.playDataCol
.updateOne({ _id: player }, {
$set: {
djname, infinitas_id,
pdata: new Binary(pdata),
check_sum,
}
@@ -125,6 +175,12 @@ export class UserService {
}).toArray();
}
getPlayLog(player: string, clock: number): Promise<PlayerPlayLog> {
return this.playLogCol.findOne({
player, clock,
});
}
async getMusicData(player: string, music_id: number, play_style: number): Promise<PlayerMusicData> {
const result = await this.musicDataCol.findOne({
player,
@@ -132,8 +188,10 @@ export class UserService {
play_style,
});
if (result)
if (result) {
result.best_score_clock ??= [-1, -1, -1, -1, -1];
return result;
}
return {
player,
@@ -145,6 +203,7 @@ export class UserService {
miss_count: [-1, -1, -1, -1, -1],
play_num: [0, 0, 0, 0, 0],
clear_num: [0, 0, 0, 0, 0],
best_score_clock: [-1, -1, -1, -1, -1],
};
}
@@ -159,4 +218,32 @@ export class UserService {
upsert: true
});
}
async getPlayerRivalData(player: string): Promise<PlayerRivalData> {
const result = await this.rivalDataCol.findOne({ _id: player });
if (result) {
if (!result.dp) result.dp = [];
if (!result.sp) result.sp = [];
return result;
}
return {
_id: player,
enabled: false,
sp: [],
dp: [],
}
}
upsertPlayerRivalData(rivalData: PlayerRivalData) {
return this.rivalDataCol.updateOne({
_id: rivalData._id
}, {
$set: rivalData
}, {
upsert: true,
});
}
}
+9
View File
@@ -0,0 +1,9 @@
export interface RivalPatch {
enabled: boolean,
};
export interface RivalPostOrDelete {
infinitas_id: string;
type: number;
};
+20 -1
View File
@@ -6,6 +6,16 @@ export interface PlayerPlayData {
pdata: Binary;
// sha256 to ^
check_sum: string;
djname: string;
infinitas_id: string;
}
export interface PlayerRivalData {
_id: string;
enabled: boolean;
sp: string[];
dp: string[];
}
export interface PlayerMusicData {
@@ -19,6 +29,9 @@ export interface PlayerMusicData {
miss_count: number[];
play_num: number[];
clear_num: number[];
// for getting ghost data;
best_score_clock: number[];
}
export interface PlayerPlayLog {
@@ -49,7 +62,7 @@ export interface PlayerPlayLog {
arrange_1: number,
assist: number,
flip: number,
ghost: Buffer,
ghost: Binary,
};
export interface PlayerCourseLog {
@@ -79,6 +92,12 @@ export interface CourseResult {
export interface PlayerCustomizeSetting {
_id: string;
items_count: {
bit: number;
ldisc: number;
infinitas_ticket: number;
infinitas_ticket_free: number;
},
customize: {
item_id: string,
item_category: number,
+1 -1
View File
@@ -97,7 +97,7 @@ function toObject(node: Record<string, unknown> | unknown[] | string): any {
return obj;
}
export function fromKBinXml(kbinxml: Buffer) {
export function fromKBinXml(kbinxml: Uint8Array) {
const xml = to_xml(kbinxml).data;
const parsedXml = parser.parse(xml);
return toObject(parsedXml);