diff --git a/src/commands/avatar.ts b/src/commands/avatar.ts new file mode 100644 index 0000000..c168acf --- /dev/null +++ b/src/commands/avatar.ts @@ -0,0 +1,32 @@ +import Avatar from "../db/Avatar"; +import Logger from "../util/Logger"; +import Interface, { Command } from "./Interface"; +const c = new Logger("/avatar", "blue"); + +export default async function handle(command: Command) { + if (!Interface.target) { + c.log("No target specified"); + return; + } + + const actionType = command.args[0]; + const avatarId = Number(command.args[1]); + const uid = Interface.target.player.db._id; + + switch (actionType) { + default: + c.log(`Usage: /avatar `); + break; + case "add": + if (!avatarId) return c.log("No avatarId specified"); + // Check if it already exists + const avatar = await Avatar.fromUID(uid, avatarId); + if (avatar) return c.log(`Avatar ${avatarId} already exists`); + Avatar.create(uid, avatarId).then(a => c.log(`Avatar ${avatarId} added to ${a.ownerUid}`)); + break; + case "remove": + if (!avatarId) return c.log("No avatarId specified"); + Avatar.remove(uid, avatarId).then(() => c.log(`Avatar ${avatarId} removed from ${uid}`)); + break; + } +} \ No newline at end of file diff --git a/src/db/Avatar.ts b/src/db/Avatar.ts new file mode 100644 index 0000000..d0b3e99 --- /dev/null +++ b/src/db/Avatar.ts @@ -0,0 +1,42 @@ +import { Avatar as AvatarI } from '../data/proto/StarRail'; +import Database from './Database'; + +type UID = number | string; + +export default class Avatar { + private constructor(public ownerUid: UID, public data: AvatarI) { + + } + + public static async create(uid: UID, baseAvatarId: number = 1001): Promise { + 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, + level: 1, + promotion: 1, + rank: 1, + skilltreeList: [], + }); + db.set("avatars", avatar); + return avatar; + } + + public static async fromUID(ownerUid: UID, baseAvatarId?: number): Promise { + const query = { ownerUid } as { ownerUid: UID, baseAvatarId?: number }; + if (baseAvatarId) query.baseAvatarId = baseAvatarId; + const db = Database.getInstance(); + return await db.getAll("avatars", query) as unknown as Avatar[]; + } + + public static async remove(ownerUid: UID, baseAvatarId: number): Promise { + const db = Database.getInstance(); + await db.delete("avatars", { ownerUid, baseAvatarId }); + } + +} \ No newline at end of file diff --git a/src/db/Database.ts b/src/db/Database.ts index 34aeb04..e371e23 100644 --- a/src/db/Database.ts +++ b/src/db/Database.ts @@ -38,6 +38,22 @@ export default class Database { } } + public async getAll(collection: string, query?: object) { + try { + const db = await Database.client.db(); + 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 }]); + } + const result = query ? await _collection.find(query).toArray() : await _collection.find().toArray(); + return result; + } catch (e) { + c.error(e as Error); + return null; + } + } + public async set(collection: string, payload: any) { try { const db = await Database.client.db(); diff --git a/src/db/Player.ts b/src/db/Player.ts index da31543..e11151c 100644 --- a/src/db/Player.ts +++ b/src/db/Player.ts @@ -1,3 +1,4 @@ +import { LineupInfo, Vector } from "../data/proto/StarRail"; import Logger from "../util/Logger"; import Account from "./Account"; import Database from "./Database"; @@ -18,6 +19,15 @@ interface PlayerI { scoin: number; worldLevel: number; } + lineup: { + curIndex: number; + lineups: LineupInfo[]; + } + posData: { + floorID: number; + planeID: number; + pos?: Vector; + } } export default class Player { diff --git a/src/server/kcp/Session.ts b/src/server/kcp/Session.ts index 94bf708..c99e920 100644 --- a/src/server/kcp/Session.ts +++ b/src/server/kcp/Session.ts @@ -45,8 +45,6 @@ export default class Session { recv = this.kcpobj.recv(); if (!recv) break; - this.c.debug(`recv ${recv.toString("hex")}`); - if (Packet.isValid(recv)) { this.handlePacket(new Packet(recv)); } @@ -58,6 +56,7 @@ export default class Session { public async handlePacket(packet: Packet) { if (Logger.VERBOSE_LEVEL >= VerboseLevel.WARNS) this.c.log(packet.protoName) + this.c.debug(packet.body); import(`../packets/${packet.protoName}`).then(mod => { mod.default(this, packet); @@ -70,10 +69,10 @@ export default class Session { } public send(name: PacketName, body: {}) { + this.c.debug(body); const packet = Packet.encode(name, body); if (!packet) return; if (Logger.VERBOSE_LEVEL >= VerboseLevel.WARNS) this.c.log(packet.protoName); - this.c.debug(`send ${packet.rawData.toString('hex')}`); this.kcpobj.send(packet.rawData); } diff --git a/src/server/packets/GetAllLineupDataCsReq.ts b/src/server/packets/GetAllLineupDataCsReq.ts index 734d2f1..08374d4 100644 --- a/src/server/packets/GetAllLineupDataCsReq.ts +++ b/src/server/packets/GetAllLineupDataCsReq.ts @@ -3,8 +3,10 @@ import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { + const lineup = session.player.db.lineup; session.send("GetAllLineupDataScRsp", { retcode: 0, - lineupList: [] - } as unknown as GetAllLineupDataScRsp); + curIndex: lineup.curIndex, + lineupList: lineup.lineups, + } as GetAllLineupDataScRsp); } \ No newline at end of file diff --git a/src/server/packets/GetAvatarDataCsReq.ts b/src/server/packets/GetAvatarDataCsReq.ts index adfdceb..137516e 100644 --- a/src/server/packets/GetAvatarDataCsReq.ts +++ b/src/server/packets/GetAvatarDataCsReq.ts @@ -2,22 +2,16 @@ import { GetAvatarDataCsReq, GetAvatarDataScRsp } from "../../data/proto/StarRai import AvatarExcelTable from "../../data/excel/AvatarExcelTable.json"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; +import Avatar from "../../db/Avatar"; export default async function handle(session: Session, packet: Packet) { const body = packet.body as GetAvatarDataCsReq; + const avatar = await Avatar.fromUID(session.player.db._id); + const dataObj = { retcode: 0, - avatarList: [{ - baseAvatarId: 1001, - equipmentUniqueId: 13501, - equipRelicList: [], - exp: 0, - level: 1, - promotion: 1, - rank: 1, - skilltreeList: [], - }], + avatarList: avatar.map(av => av.data), isAll: body.isGetAll } as GetAvatarDataScRsp; diff --git a/src/server/packets/GetCurBattleInfoCsReq.ts b/src/server/packets/GetCurBattleInfoCsReq.ts index 15dc9d4..1c0c125 100644 --- a/src/server/packets/GetCurBattleInfoCsReq.ts +++ b/src/server/packets/GetCurBattleInfoCsReq.ts @@ -1,24 +1,36 @@ -import { AvatarType, BattleEndStatus, GetCurBattleInfoScRsp } from "../../data/proto/StarRail"; +import { AvatarType, BattleAvatar, BattleEndStatus, GetCurBattleInfoScRsp } from "../../data/proto/StarRail"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { + const _lineup = session.player.db.lineup; + const lineup = _lineup.lineups[_lineup.curIndex]; + session.send("GetCurBattleInfoScRsp", { retcode: 0, - avatarList: [{ - avatarType: AvatarType.AVATAR_FORMAL_TYPE, - id: 1001, - level: 1, - rank: 1, - index: 1, - hp: 100, - sp: 100, - promotion: 1, - }], - stageId: 10000, + avatarList: lineup.avatarList.map(list => { + return { + avatarType: list.avatarType, + equipmentList: [{ + id: 20003, + level: 1, + promotion: 1, + rank: 1 + }], + hp: list.hp, + id: list.id, + index: list.slot, + level: 1, + promotion: 1, + rank: 1, + relicList: [], + skilltreeList: [] + } as unknown as BattleAvatar; + }), + stageId: 1, logicRandomSeed: 2503, battleInfo: {}, lastEndStatus: BattleEndStatus.BATTLE_END_WIN, - lastEventId: 0 + lastEventId: 1 } as GetCurBattleInfoScRsp); } \ No newline at end of file diff --git a/src/server/packets/GetCurLineupDataCsReq.ts b/src/server/packets/GetCurLineupDataCsReq.ts index 7f1a6e4..59e51b6 100644 --- a/src/server/packets/GetCurLineupDataCsReq.ts +++ b/src/server/packets/GetCurLineupDataCsReq.ts @@ -3,23 +3,10 @@ import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { + const _lineup = session.player.db.lineup; + const lineup = _lineup.lineups[_lineup.curIndex]; session.send("GetCurLineupDataScRsp", { retcode: 0, - lineup: { - avatarList: [{ - slot: 1, - avatarType: AvatarType.AVATAR_FORMAL_TYPE, - id: 1001, - hp: 100, - sp: 100, - satiety: 100 - }], - index: 1, - isVirtual: false, - mp: 100, - name: "lineuprspname", - planeId: 10000, - leaderSlot: 1 - } + lineup } as GetCurLineupDataScRsp); } \ No newline at end of file diff --git a/src/server/packets/GetCurSceneInfoCsReq.ts b/src/server/packets/GetCurSceneInfoCsReq.ts index ae5b40f..a9fc761 100644 --- a/src/server/packets/GetCurSceneInfoCsReq.ts +++ b/src/server/packets/GetCurSceneInfoCsReq.ts @@ -3,11 +3,12 @@ import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { + const posData = session.player.db.posData; session.send("GetCurSceneInfoScRsp", { retcode: 0, scene: { - planeId: 10000, - floorId: 10000000, + planeId: posData.planeID, + floorId: posData.floorID, entityList: [], entityBuffList: [], entryId: 10001, diff --git a/src/server/packets/PlayerLoginCsReq.ts b/src/server/packets/PlayerLoginCsReq.ts index 7c421ad..4dbd8e7 100644 --- a/src/server/packets/PlayerLoginCsReq.ts +++ b/src/server/packets/PlayerLoginCsReq.ts @@ -1,4 +1,5 @@ -import { PlayerBasicInfo, PlayerLoginCsReq, PlayerLoginScRsp } from "../../data/proto/StarRail"; +import { AvatarType, ExtraLineupType, PlayerBasicInfo, PlayerLoginCsReq, PlayerLoginScRsp } from "../../data/proto/StarRail"; +import Avatar from "../../db/Avatar"; import Player from "../../db/Player"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; @@ -21,9 +22,11 @@ import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { const body = packet.body as PlayerLoginCsReq; - const plr = await Player.fromUID(session.player.db._id)!; - if (!plr!.db.basicInfo) { - plr!.db.basicInfo = { + const plr = await Player.fromUID(session.player.db._id); + if (!plr) return; + + if (!plr.db.basicInfo) { + plr.db.basicInfo = { exp: 0, level: 1, hcoin: 0, @@ -33,7 +36,40 @@ export default async function handle(session: Session, packet: Packet) { stamina: 100, worldLevel: 1, } - plr!.save(); + plr.save(); + } + + if (!plr.db.lineup) { + Avatar.create(plr.db._id); + plr.db.lineup = { + curIndex: 0, + lineups: [{ + avatarList: [{ + avatarType: AvatarType.AVATAR_FORMAL_TYPE, + hp: 10000, + sp: 10000, + satiety: 100, + slot: 0, + id: 1001 + }], + planeId: 10001, + isVirtual: false, + name: "Default Party", + index: 0, + leaderSlot: 0, + mp: 100, + extraLineupType: ExtraLineupType.LINEUP_NONE + }] + } + plr.save(); + } + + if (!plr.db.posData) { + plr.db.posData = { + floorID: 10001001, + planeID: 10001 + } + plr.save(); } session.send("PlayerLoginScRsp", { diff --git a/src/util/ProtoFactory.ts b/src/util/ProtoFactory.ts index 058a93f..a2d63d7 100644 --- a/src/util/ProtoFactory.ts +++ b/src/util/ProtoFactory.ts @@ -56,7 +56,7 @@ export default class ProtoFactory { } } - c.debug(`Initialized with " ${messageTypeMap.size} types`); + c.debug(`Initialized with ${messageTypeMap.size} types`); //c.log(this.getName(types.PlayerLoginScRsp)) return; diff --git a/src/util/excel/AvatarExcel.ts b/src/util/excel/AvatarExcel.ts new file mode 100644 index 0000000..16bff7e --- /dev/null +++ b/src/util/excel/AvatarExcel.ts @@ -0,0 +1,69 @@ +import _AvatarExcelTable from "../../data/excel/AvatarExcelTable.json"; +const AvatarExcelTable = _AvatarExcelTable as { [key: string]: AvatarExcelTableEntry }; + +export default class AvatarExcel { + private constructor() { } + + public static fromId(id: number): AvatarExcelTableEntry { + return AvatarExcelTable[id]; + } + + public static fromIds(ids: number[]): AvatarExcelTableEntry[] { + return ids.map(id => AvatarExcel.fromId(id)); + } +} + +interface Reward { + ItemID: number, + ItemNum: number, +} + +interface TextMap { + hash: number; +} + +export interface AvatarExcelTableEntry { + AvatarID: number; + AvatarName: TextMap; + AvatarFullName: TextMap; + AdventurePlayerID: number; + AvatarVOTag: string; + Rarity: string; + JsonPath: string; + NatureID: number; + DamageType: string; + SPNeed: { + RawValue: number; + }; + ExpGroup: number; + MaxPromotion: number; + MaxRank: number; + RankIDList: number[]; + RewardList: Reward[]; + RewardListMax: Reward[]; + SkillList: number[]; + AvatarBaseType: string; + DefaultAvatarModelPath: string; + DefaultAvatarHeadIconPath: string; + AvatarSideIconPath: string; + ActionAvatarHeadIconPath: string; + AvatarBaseTypeIconPath: string; + AvatarDialogHalfImagePath: string; + UltraSkillCutInPrefabPath: string; + UIAvatarModelPath: string; + ManikinJsonPath: string; + AvatarDesc: TextMap; + AIPath: string; + SkilltreePrefabPath: string; + DamageTypeResistance: never[]; + Release: boolean; + SideAvatarHeadIconPath: string; + WaitingAvatarHeadIconPath: string; + AvatarCutinImgPath: string; + AvatarCutinBgImgPath: string; + AvatarCutinFrontImgPath: string; + AvatarCutinIntroText: TextMap; + GachaResultOffset: number[]; + AvatarDropOffset: number[]; + AvatarTrialOffset: number[]; +} \ No newline at end of file