From 4738e7c44a4b167f39b81e0c4fa721767d8c872d Mon Sep 17 00:00:00 2001 From: GanyusLeftHorn <1244229+GanyusLeftHorn@users.noreply.github.com> Date: Thu, 4 Aug 2022 02:41:34 +0200 Subject: [PATCH] I don't want to talk about it. (#35) --- src/commands/avatar.ts | 26 ++- src/db/Avatar.ts | 188 +++++++++++++++----- src/db/Database.ts | 12 +- src/db/Inventory.ts | 16 +- src/db/Player.ts | 64 +++---- src/server/kcp/Session.ts | 11 +- src/server/packets/GetAvatarDataCsReq.ts | 17 +- src/server/packets/GetCurLineupDataCsReq.ts | 29 +-- src/server/packets/JoinLineupCsReq.ts | 13 +- src/server/packets/PlayerLoginCsReq.ts | 3 +- 10 files changed, 237 insertions(+), 142 deletions(-) diff --git a/src/commands/avatar.ts b/src/commands/avatar.ts index 7485bc6..382f3de 100644 --- a/src/commands/avatar.ts +++ b/src/commands/avatar.ts @@ -13,6 +13,7 @@ export default async function handle(command: Command) { const actionType = command.args[0]; const avatarId = Number(command.args[1]); const uid = Interface.target.player.db._id; + const player = Interface.target.player; switch (actionType) { default: { @@ -20,21 +21,32 @@ export default async function handle(command: Command) { break; } case "add": { - if (!avatarId) return c.log("No avatarId specified"); + if (!avatarId) { + return c.log("No avatarId specified"); + } + // Check if it already exists - const avatar = await Avatar.fromUID(uid, avatarId); - if (avatar.length > 0) return c.log(`Avatar ${avatarId} already exists`); - Avatar.create(uid, avatarId).then(a => c.log(`Avatar ${avatarId} added to ${a.ownerUid}`)); + if (await Avatar.hasAvatar(player, avatarId)) { + return c.log(`Avatar ${avatarId} already exists`); + } + + await Avatar.addAvatarToPlayer(player, avatarId).then(a => c.log(`Avatar ${avatarId} added to ${a.db.ownerUid}`)); break; } case "remove": { if (!avatarId) return c.log("No avatarId specified"); - Avatar.remove(uid, avatarId).then(() => c.log(`Avatar ${avatarId} removed from ${uid}`)); + await Avatar.removeAvatarFromPlayer(player, avatarId).then(() => c.log(`Avatar ${avatarId} removed from ${uid}`)); break; } case "giveall": { for (const id in AvatarExcelTable) { - await Avatar.create(uid, parseInt(id)); + const avatarId = Number(id); + // Let's not brick our account. + if (avatarId>= 8000) { + continue; + } + + await Avatar.addAvatarToPlayer(player, avatarId); } c.log(`All avatars added to ${uid}`); break; @@ -42,7 +54,7 @@ export default async function handle(command: Command) { case "removeall": { for (const id in AvatarExcelTable) { if (Number(id) !== 1001) { - await Avatar.remove(uid, parseInt(id)); + await Avatar.removeAvatarFromPlayer(player, parseInt(id)); } } c.log(`All avatars removed from ${uid}`); diff --git a/src/db/Avatar.ts b/src/db/Avatar.ts index 9c656d7..363ebe0 100644 --- a/src/db/Avatar.ts +++ b/src/db/Avatar.ts @@ -1,68 +1,162 @@ -import { Avatar as AvatarI, AvatarType, LineupAvatar } from '../data/proto/StarRail'; +// import { Avatar as AvatarI, AvatarType, LineupAvatar } from '../data/proto/StarRail'; +import { AvatarSkillTree, AvatarType, EquipRelic, Avatar as AvatarProto } from '../data/proto/StarRail'; import Logger from '../util/Logger'; import Database from './Database'; import Player, { LineupI } from './Player'; const c = new Logger("Avatar"); type UID = number | string; -export default class Avatar { - private constructor(public ownerUid: UID, public data: AvatarI, public lineup: LineupAvatar) { +interface AvatarI { + ownerUid: number, + baseAvatarId: number, + avatarType: AvatarType, + level: number, + exp: number, + promotion: number, + rank: number, + equipmentUniqueId: number, + equipRelicList: EquipRelic[], + skilltreeList: AvatarSkillTree[], + fightProps: { + hp: number, + sp: number, + satiety: number + } +} +export default class Avatar { + public readonly player: Player; + public readonly db: AvatarI; + + private constructor(player: Player, db: AvatarI) { + this.player = player; + this.db = db; } - public static async create(uid: UID, baseAvatarId: number = 1001, slot: number = -1): Promise { + /******************************************************************************** + Create and fetch avatars from the database. + ********************************************************************************/ + + public static async loadAvatarsForPlayer(player: Player) : Promise { + // Read avatars for this player from the database. const db = Database.getInstance(); - // Check if already exists - const existing = await Avatar.fromUID(uid, baseAvatarId); - if (existing.length > 0) return existing[0]; - const avatar = new Avatar(uid, { - baseAvatarId, - equipmentUniqueId: 20003, // TODO: Placeholder while we work on inventory system - equipRelicList: [], - exp: 0, + const avatars = await db.getAll("avatars", { ownerUid: player.uid }) as unknown as AvatarI[]; + + // If this player doesn't have any avatars yet, add a default. + if (avatars.length < 1) { + avatars.push({ + ownerUid: player.uid, + baseAvatarId: 1001, + avatarType: AvatarType.AVATAR_FORMAL_TYPE, + level: 1, + exp: 1, + promotion: 1, + rank: 1, + equipmentUniqueId: 20003, + equipRelicList: [], + skilltreeList: [], + fightProps: { + hp: 10000, + sp: 10000, + satiety: 100 + } + } as AvatarI); + } + + // Construct Avatar instances. + const res: Avatar[] = [] + for (const avatar of avatars) { + res.push(new Avatar(player, avatar)); + } + + // Done. + return res; + } + + public static async loadAvatarForPlayer(player: Player, baseAvatarId: number) : Promise { + // Fetch the given avatar from the database. + const db = Database.getInstance(); + const avatar = await db.get("avatars", { ownerUid: player.uid, baseAvatarId: baseAvatarId }) as unknown as AvatarI; + + // Sanity check. + if (!avatar) { + throw new Error(`Avatar ${baseAvatarId} does not exist for player ${player.uid}. This should never happen. Check your logic at the callsite.`); + } + + // Done. + return new Avatar(player, avatar); + } + + public static async hasAvatar(player: Player, baseAvatarId: number) : Promise { + // Fetch the given avatar from the database. + const db = Database.getInstance(); + const avatar = await db.get("avatars", { ownerUid: player.uid, baseAvatarId: baseAvatarId }) as unknown as AvatarI; + + // Return. + return avatar ? true : false; + } + + public static async addAvatarToPlayer(player: Player, baseAvatarId: number) : Promise { + const db = Database.getInstance(); + + // Make sure the player doesn't already have that avatar. + const existingAvatar = await db.get("avatars", { ownerUid: player.uid, baseAvatarId: baseAvatarId }) as unknown as AvatarI; + if (existingAvatar) { + return new Avatar(player, existingAvatar); + } + + // Insert. + const data : AvatarI = { + ownerUid: player.uid, + baseAvatarId: baseAvatarId, + avatarType: AvatarType.AVATAR_FORMAL_TYPE, level: 1, + exp: 1, promotion: 1, rank: 1, + equipmentUniqueId: 20003, + equipRelicList: [], skilltreeList: [], - }, { - avatarType: AvatarType.AVATAR_FORMAL_TYPE, - hp: 10000, - id: baseAvatarId, - satiety: 100, - slot: slot, - sp: 10000 - }); - db.set("avatars", avatar); - return avatar; - } - - public static async fromUID(ownerUid: UID, baseAvatarId?: number): Promise { - const query = { ownerUid } as { ownerUid: UID, "data.baseAvatarId"?: number }; - if (baseAvatarId) query['data.baseAvatarId'] = baseAvatarId; - const db = Database.getInstance(); - return await db.getAll("avatars", query) as unknown as Avatar[]; - } - - public static async fromLineup(uid: UID, lineup: LineupI): Promise { - try { - const avatarList: Array = []; - - for (let i = 0; i < lineup.avatarList.length; i++) { - const avatarId = lineup.avatarList[i]; - const avatar = await Avatar.fromUID(uid, avatarId); - avatarList.push(avatar[0]); + fightProps: { + hp: 10000, + sp: 10000, + satiety: 100 } - - return await Promise.all(avatarList); - } catch (e) { - c.error(e as Error); - return []; - } + }; + await db.set("avatars", data); + return new Avatar(player, data); } - public static async remove(ownerUid: UID, baseAvatarId: number): Promise { + public static async removeAvatarFromPlayer(player: Player, baseAvatarId: number) { const db = Database.getInstance(); - await db.delete("avatars", { ownerUid, "data.baseAvatarId": baseAvatarId }); + await db.delete("avatars", { ownerUid: player.uid, baseAvatarId: baseAvatarId }); } + public static async getAvatarsForLineup(player: Player, lineup: LineupI) : Promise { + const res: Avatar[] = []; + + // Load all avatars in this lineup. + for (const avatarId of lineup.avatarList) { + res.push(await Avatar.loadAvatarForPlayer(player, avatarId)); + } + + // Done. + return res; + } + + /******************************************************************************** + Get avatar info. + ********************************************************************************/ + public asAvatarProto() : AvatarProto { + return { + baseAvatarId: this.db.baseAvatarId, + exp: this.db.exp, + level: this.db.level, + promotion: this.db.promotion, + rank: this.db.rank, + skilltreeList: this.db.skilltreeList, + equipmentUniqueId: this.db.equipmentUniqueId, + equipRelicList: this.db.equipRelicList + }; + } } \ No newline at end of file diff --git a/src/db/Database.ts b/src/db/Database.ts index e371e23..3b8bec8 100644 --- a/src/db/Database.ts +++ b/src/db/Database.ts @@ -28,7 +28,7 @@ export default class Database { const _collection = db.collection(collection); if (!(await db.listCollections({ name: collection }).toArray()).length) { c.warn(`Collection ${collection} does not exist. Creating...`); - await _collection.createIndexes([{ key: { id: 1 }, unique: true }]); + await _collection.createIndexes([{ key: { _id: 1 } }]); } const result = query ? await _collection.findOne(query) : await _collection.findOne(); return result; @@ -44,7 +44,7 @@ export default class Database { const _collection = db.collection(collection); if (!(await db.listCollections({ name: collection }).toArray()).length) { c.warn(`Collection ${collection} does not exist. Creating...`); - await _collection.createIndexes([{ key: { id: 1 }, unique: true }]); + await _collection.createIndexes([{ key: { _id: 1 } }]); } const result = query ? await _collection.find(query).toArray() : await _collection.find().toArray(); return result; @@ -60,10 +60,12 @@ export default class Database { const _collection = db.collection(collection); if (!(await db.listCollections({ name: collection }).toArray()).length) { c.warn(`Collection ${collection} does not exist. Creating...`); - await _collection.createIndexes([{ key: { id: 1 }, unique: true }]); + await _collection.createIndexes([{ key: { _id: 1 } }]); } return await _collection.insertOne(payload); } catch (e) { + c.error("In set.") + c.error(payload) c.error(e as Error); } } @@ -74,7 +76,7 @@ export default class Database { const _collection = db.collection(collection); if (!(await db.listCollections({ name: collection }).toArray()).length) { c.warn(`Collection ${collection} does not exist. Creating...`); - await _collection.createIndexes([{ key: { id: 1 }, unique: true }]); + await _collection.createIndexes([{ key: { _id: 1 } }]); } return await _collection.deleteOne(query); } catch (e) { @@ -88,7 +90,7 @@ export default class Database { const _collection = db.collection(collection); if (!(await db.listCollections({ name: collection }).toArray()).length) { c.warn(`Collection ${collection} does not exist. Creating...`); - await _collection.createIndexes([{ key: { id: 1 }, unique: true }]); + await _collection.createIndexes([{ key: { _id: 1 } }]); } return await _collection.updateOne(query, { $set: payload }, { upsert: true }); } catch (e) { diff --git a/src/db/Inventory.ts b/src/db/Inventory.ts index bc95e4c..696de14 100644 --- a/src/db/Inventory.ts +++ b/src/db/Inventory.ts @@ -3,7 +3,6 @@ import Logger from "../util/Logger"; import Database from "./Database"; import Player from "./Player"; import ItemExcel from "../util/excel/ItemExcel"; -import { Timestamp } from "mongodb"; const c = new Logger("Inventory"); @@ -165,10 +164,6 @@ export default class Inventory { if (!itemData || itemData.ItemType != "Material") { return; } - const t = itemData.ItemType; - if (t != "Material") { - return; - } // Get current item count for this ID and calculate new count. const currentCount = this.db.materials[id] ?? 0; @@ -189,6 +184,11 @@ export default class Inventory { public async addEquipment(equipment: number | Equipment) { // If the parameter is a number, add a new equipment with this item ID as base. if (typeof(equipment) == "number") { + // Sanity check. + if (ItemExcel.fromId(equipment)?.ItemType != "Equipment") { + return; + } + const equip : Equipment = { tid: equipment, uniqueId: this.db.nextItemUid++, @@ -292,6 +292,9 @@ export default class Inventory { // Find index to delete. const toDelete: number = (typeof(equipment) == "number") ? equipment : equipment.uniqueId; const index = this.db.equipments.findIndex(i => i.uniqueId == toDelete); + if (index == -1) { + return; + } // Delete and save. this.db.equipments.splice(index, 1); @@ -309,6 +312,9 @@ export default class Inventory { // Find index to delete. const toDelete: number = (typeof(relic) == "number") ? relic : relic.uniqueId; const index = this.db.relics.findIndex(i => i.uniqueId == toDelete); + if (index == -1) { + return; + } // Delete and save. this.db.relics.splice(index, 1); diff --git a/src/db/Player.ts b/src/db/Player.ts index 42437eb..d23647c 100644 --- a/src/db/Player.ts +++ b/src/db/Player.ts @@ -1,5 +1,5 @@ import Session from "../server/kcp/Session"; -import { AvatarType, ExtraLineupType, HeroBasicType, LineupInfo, Vector } from "../data/proto/StarRail"; +import { AvatarType, ExtraLineupType, HeroBasicType, LineupAvatar, LineupInfo, Vector } from "../data/proto/StarRail"; import Logger from "../util/Logger"; import Account from "./Account"; import Avatar from "./Avatar"; @@ -67,34 +67,35 @@ export default class Player { public static async fromToken(session: Session, token: string): Promise { const db = Database.getInstance(); - const plr = await db.get("players", { token }) as unknown as PlayerI; - if (!plr) return Player.fromUID(session, (await Account.fromToken(token))?.uid || Math.round(Math.random() * 50000)); + const plr = await db.get("players", { token: token }) as unknown as PlayerI; + if (!plr) return await Player.fromUID(session, (await Account.fromToken(token))?.uid || Math.round(Math.random() * 50000)); return new Player(session, plr); } public async getLineup(lineupIndex?: number): Promise { - const curIndex = this.db.lineup.curIndex; - const lineup = this.db.lineup.lineups[lineupIndex || curIndex]; - const avatars = await Avatar.fromLineup(this.uid, lineup); - let slot = 0; - avatars.forEach(avatar => { - // Fallback lineup - if (!avatar) return; // Matsuko. - if (!avatar.lineup) avatar.lineup = { - avatarType: AvatarType.AVATAR_FORMAL_TYPE, - hp: 10000, - id: 1001, - satiety: 100, + // Get avatar data. + const index = lineupIndex ?? this.db.lineup.curIndex; + const lineup = this.db.lineup.lineups[index]; + const avatars = await Avatar.getAvatarsForLineup(this, lineup); + + // Construct LineupInfo. + const lineupAvatars : LineupAvatar[] = []; + for (let slot = 0; slot < avatars.length; slot++) { + lineupAvatars.push({ slot: slot, - sp: 10000 - } - avatar.lineup.slot = slot++; - }); + avatarType: avatars[slot].db.avatarType, + id: avatars[slot].db.baseAvatarId, + hp: avatars[slot].db.fightProps.hp, + sp: avatars[slot].db.fightProps.sp, + satiety: avatars[slot].db.fightProps.satiety + }); + } + return { ...lineup, index: 0, - avatarList: avatars.map(x => x.lineup) + avatarList: lineupAvatars } } @@ -166,23 +167,26 @@ export default class Player { name: "", planeId: 10001 } + const LINEUPS = 6; - dataObj.lineup = { - curIndex: 0, - lineups: {} - } - for (let i = 0; i <= LINEUPS; i++) { - const copy = baseLineup; - copy.index = 0; - copy.name = `Team ${i}`; + for (let i = 0; i < LINEUPS; i++) { + const copy = { + ...baseLineup, + index: 0, + name: `Team ${i}` + }; dataObj.lineup.lineups[i] = copy; } - await Avatar.create(uid, 1001, 0); + const player = new Player(session, dataObj); + + await Avatar.addAvatarToPlayer(player, 1001); + // await Avatar.create(uid, 1001, 0); + // Save to database and return. await db.set("players", dataObj); - return new Player(session, dataObj); + return player; } public async save() { diff --git a/src/server/kcp/Session.ts b/src/server/kcp/Session.ts index d5afad7..194f38f 100644 --- a/src/server/kcp/Session.ts +++ b/src/server/kcp/Session.ts @@ -55,7 +55,7 @@ export default class Session { if (!recv) break; if (Packet.isValid(recv)) { - this.handlePacket(new Packet(recv)); + await this.handlePacket(new Packet(recv)); } } while (recv) @@ -68,8 +68,8 @@ export default class Session { this.c.verbL(packet.body); this.c.verbH(packet.rawData); - import(`../packets/${packet.protoName}`).then(mod => { - mod.default(this, packet); + await import(`../packets/${packet.protoName}`).then(async mod => { + await mod.default(this, packet); }).catch(e => { if (e.code === 'MODULE_NOT_FOUND') this.c.warn(`Unhandled packet: ${packet.protoName}`); else this.c.error(e); @@ -79,12 +79,13 @@ export default class Session { } public async sync() { - const avatars = await Avatar.fromUID(this.player.db._id); + const avatars = await Avatar.loadAvatarsForPlayer(this.player); + //const avatars = await Avatar.fromUID(this.player.db._id); const inventory = await this.player.getInventory(); this.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ avatarSync: { - avatarList: avatars.map(x => x.data) + avatarList: avatars.map(x => x.asAvatarProto()) }, materialList: inventory.getMaterialList(), equipmentList: inventory.getEquipmentList(), diff --git a/src/server/packets/GetAvatarDataCsReq.ts b/src/server/packets/GetAvatarDataCsReq.ts index 1453dfb..bba73fb 100644 --- a/src/server/packets/GetAvatarDataCsReq.ts +++ b/src/server/packets/GetAvatarDataCsReq.ts @@ -1,26 +1,19 @@ import { GetAvatarDataCsReq, GetAvatarDataScRsp } from "../../data/proto/StarRail"; -import AvatarExcelTable from "../../data/excel/AvatarExcelTable.json"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; import AvatarDb from "../../db/Avatar"; -import { Avatar } from "../../data/proto/StarRail"; - export default async function handle(session: Session, packet: Packet) { const body = packet.body as GetAvatarDataCsReq; + const avatars = await AvatarDb.loadAvatarsForPlayer(session.player); - const avatar = await AvatarDb.fromUID(session.player.db._id); - - console.log(avatar.length) const dataObj = { retcode: 0, - avatarList: avatar.map(av => Avatar.fromPartial(av.data)), + avatarList: avatars.map(av => av.asAvatarProto()), isAll: body.isGetAll }; - Object.values(AvatarExcelTable).forEach(avatar => { - // dataObj.avatarList.push() - }); - - session.send(GetAvatarDataScRsp, dataObj); + // Make sure we wait for this to send. + // GetAvatarDataScRsp HAS to be sent immediately after the Req. + await session.send(GetAvatarDataScRsp, dataObj); } \ No newline at end of file diff --git a/src/server/packets/GetCurLineupDataCsReq.ts b/src/server/packets/GetCurLineupDataCsReq.ts index 0968fce..8e28e95 100644 --- a/src/server/packets/GetCurLineupDataCsReq.ts +++ b/src/server/packets/GetCurLineupDataCsReq.ts @@ -1,39 +1,12 @@ import { AvatarType, ExtraLineupType, GetCurLineupDataScRsp } from "../../data/proto/StarRail"; -import Avatar from "../../db/Avatar"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { let lineup = await session.player.getLineup(); - // This is a HORRIBLE solution, but tbh I just can't reproduce the bug so: - if (!lineup) { - session.c.error("Error! lineup is undefined. Falling back to default lineup.", false); - lineup = { - avatarList: [{ - avatarType: AvatarType.AVATAR_FORMAL_TYPE, - hp: 10000, - id: 1001, - sp: 10000, - satiety: 100, - slot: 0, - }], - extraLineupType: ExtraLineupType.LINEUP_NONE, - index: 0, - isVirtual: false, - leaderSlot: 0, - mp: 100, - planeId: 10001, - name: "Fallback" - } - session.player.setLineup(lineup, 0, 0); - session.player.save(); - } - session.send(GetCurLineupDataScRsp, { retcode: 0, - lineup: { - ...lineup, - } + lineup: lineup } as GetCurLineupDataScRsp); } \ No newline at end of file diff --git a/src/server/packets/JoinLineupCsReq.ts b/src/server/packets/JoinLineupCsReq.ts index 50c5b87..a8a4184 100644 --- a/src/server/packets/JoinLineupCsReq.ts +++ b/src/server/packets/JoinLineupCsReq.ts @@ -8,9 +8,16 @@ export default async function handle(session: Session, packet: Packet) { const body = packet.body as JoinLineupCsReq; session.send(JoinLineupScRsp, { retcode: 0 }); + // Replace avatar in the player's lineup. + const slot = body.slot ?? 0; + session.player.db.lineup.lineups[session.player.db.lineup.curIndex].avatarList[slot] = body.baseAvatarId; + + /* + let lineup = await session.player.getLineup(); - const slot = body.slot || 0; const avatarList = []; + + // What in the fuck is the purpose of this loop supposed to be?! for (const avatarId in lineup) { const avatar = await Avatar.fromUID(session.player.db._id, Number(avatarId)); if (avatar.length === 0) return session.c.warn(`Avatar ${body.baseAvatarId} not found`); @@ -27,10 +34,12 @@ export default async function handle(session: Session, packet: Packet) { }; if (body.extraLineupType) lineup.extraLineupType = body.extraLineupType; session.player.setLineup(lineup); + */ + session.player.save(); session.send(SyncLineupNotify, { - lineup: lineup, + lineup: await session.player.getLineup(), reasonList: [SyncLineupReason.SYNC_REASON_NONE] } as SyncLineupNotify); } \ No newline at end of file diff --git a/src/server/packets/PlayerLoginCsReq.ts b/src/server/packets/PlayerLoginCsReq.ts index 616b528..e9cd8df 100644 --- a/src/server/packets/PlayerLoginCsReq.ts +++ b/src/server/packets/PlayerLoginCsReq.ts @@ -45,7 +45,8 @@ export default async function handle(session: Session, packet: Packet) { } if (!plr.db.lineup) { - await Avatar.create(plr.db._id, 1001, 0); + await Avatar.addAvatarToPlayer(plr, 1001); + //await Avatar.create(plr.db._id, 1001, 0); const baseLineup = { avatarList: [1001], extraLineupType: ExtraLineupType.LINEUP_NONE,