diff --git a/src/commands/item.ts b/src/commands/item.ts new file mode 100644 index 0000000..39fcd14 --- /dev/null +++ b/src/commands/item.ts @@ -0,0 +1,104 @@ +import { Equipment } from "../data/proto/StarRail"; +import Player from "../db/Player"; +import ItemExcel from "../util/excel/ItemExcel"; +import Logger from "../util/Logger"; +import Interface, { Command } from "./Interface"; +const c = new Logger("/item", "blue"); + +export default async function handle(command: Command) { + if (!Interface.target) { + c.log("No target specified"); + return; + } + + const player = Interface.target.player; + const actionType = command.args[0]; + const itemId = Number(command.args[1]); + + let count: number = 1; + let level: number = 1; + let rank: number = 1; + let promotion: number = 1; + + for (let i = 2; i < command.args.length; i++) { + const arg = command.args[i]; + const number = Number(command.args[i].substring(1)); + + if (arg.startsWith("x")) { + count = number; + } + else if (arg.startsWith("l")) { + level = number; + } + else if (arg.startsWith("r")) { + rank = number; + } + else if (arg.startsWith("p")) { + promotion = number; + } + } + + switch (actionType) { + case "give": { + await handleGive(player, itemId, count, level, rank, promotion); + break; + } + case "giveall": { + await handleGiveAll(player); + break; + } + default: { + c.log(`Usage: /item [x|l|r|p]*`); + break; + } + } +} + +async function handleGive(player: Player, itemId: number, count:number, level: number, rank: number, promotion: number) { + if (!itemId) { + return c.log("No avatarId specified"); + } + + // Check if this item exists. + const itemData = ItemExcel.fromId(itemId); + if (!itemData) { + return c.log(`Item ID ${itemId} does not exist.`); + } + + const inventory = await player.getInventory(); + switch (itemData.ItemType) { + case "Material": + await inventory.addMaterial(itemId, count); + break; + case "Equipment": + for (let i = 0; i < count; i++) { + await inventory.addEquipment({ + tid: itemId, + uniqueId: 0, + level: level, + rank: rank, + exp: 1, + isProtected: false, + promotion: promotion, + baseAvatarId: 0 + } as Equipment); + } + break; + default: + return c.log(`Unsupported item type: ${itemData.ItemType}.`); + break; + } + + c.log(`Added ${count} of item ${itemId} to player ${player.uid}`); +} + +async function handleGiveAll(player: Player) { + const inventory = await player.getInventory(); + + for (const entry of ItemExcel.all()) { + const count = entry.ItemType == "Material" ? 100 : 1; + await inventory.addItem(entry.ID, count); + } + + c.log(`All materials added to ${player.uid}`); +} \ No newline at end of file diff --git a/src/db/Inventory.ts b/src/db/Inventory.ts new file mode 100644 index 0000000..bc95e4c --- /dev/null +++ b/src/db/Inventory.ts @@ -0,0 +1,369 @@ +import { Equipment, Item, Material, PlayerSyncScNotify, Relic } from "../data/proto/StarRail"; +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"); + +export interface PayItemData { + id: number, + count: number +} + +interface InventoryI { + _id: number, + nextItemUid: number, + materials: { [key: number]: number }, + relics: Relic[], + equipments: Equipment[] +} + +export default class Inventory { + public readonly player : Player; + public readonly db: InventoryI; + + private constructor(player: Player, db: InventoryI) { + this.player = player; + this.db = db; + } + + public static async loadOrCreate(player: Player) : Promise { + // Try to load the player's inventory from the database. + const db = Database.getInstance(); + const inventory = await db.get("inventory", { _id: player.uid }) as unknown as InventoryI; // How to get rid of this ugly fuck?! + + // If successfull, we are done. + if (inventory) { + return new Inventory(player, inventory); + } + + // Otherwise, we create a default inventory. + const data : InventoryI = { + _id: player.uid, + nextItemUid: 1, + materials: {}, + relics: [], + equipments: [] + }; + await db.set("inventory", data); + return new Inventory(player, data); + } + + public async save() { + const db = Database.getInstance(); + await db.update("inventory", { _id: this.db._id }, this.db); + } + + /******************************************************************************** + Get inventory info. + ********************************************************************************/ + /** + * Get list of all `Material`s as proto. + * @returns List of materials. + */ + public getMaterialList() : Material[] { + const res: Material[] = []; + + Object.keys(this.db.materials).forEach(key => { + res.push({ tid: Number(key), num: this.db.materials[Number(key)] } as Material); + }); + + return res; + } + + /** + * Get list of all `Equipment`s as proto. + * @returns List of equipments. + */ + public getEquipmentList() : Equipment[] { + return this.db.equipments; + } + + /** + * Get list of all `Relic`s as proto. + * @returns List of relics. + */ + public getRelicsList() : Relic[] { + return this.db.relics; + } + + /** + * Returns the count of the given item (material or virtual) in the player's inventory. + * @param id The item id. + * @returns The count in the player's inventory. + */ + public async getItemCount(id: number) : Promise { + // Get item data. + const itemData = ItemExcel.fromId(id); + if (!itemData) { + return 0; + } + + switch (itemData.ItemType) { + case "Virtual": return 0; // ToDo: Handle virtual items. + case "Material": return this.db.materials[id] ?? 0; + } + + return 0; + } + + /******************************************************************************** + Add items to the inventory. + ********************************************************************************/ + + /** + * Add the given amount of the given item to the player's inventory. + * @param id The item id. For equipment and relics, this is the base id. + * @param count The amount of items to add. + */ + public async addItem(id: number, count: number) { + // Get info for the particular item we are trying to add. + const itemData = ItemExcel.fromId(id); + if (!itemData) { + return; + } + + // Handle adding depending on item type. + const t = itemData.ItemType; + if (t == "Virtual") { + await this.addVirtualItem(id, count); + } + else if (t == "Material") { + await this.addMaterial(id, count); + } + else if (t == "Equipment") { + for (let i = 0; i < count; i++) { + await this.addEquipment(id); + } + } + else if (t == "Relic") { + for (let i = 0; i < count; i++) { + await this.addRelic(id); + } + } + } + + /** + * Adds the given amount of the virtual item with the given id to the player's inventory. + * @param id The item id. + * @param count The amount. + */ + public async addVirtualItem(id: number, count: number) { + // ToDo: Figure out which virtual item ID is what. + } + + /** + * Adds the given amount of the material with the given id to the player's inventory. + * @param id The material id. + * @param count The amount. + */ + public async addMaterial(id: number, count: number) { + // Get info for the particular item we are trying to add. + const itemData = ItemExcel.fromId(id); + 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; + const newCount = Math.min(currentCount + count, itemData.PileLimit); + + // Update. + this.db.materials[id] = newCount; + await this.save(); + + // Send update. + this.sendMaterialUpdate(); + } + + /** + * Adds the given equipment to the player's inventory. + * @param equipment Either an `Equipment`, or the base id. + */ + 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") { + const equip : Equipment = { + tid: equipment, + uniqueId: this.db.nextItemUid++, + level: 1, + rank: 1, + exp: 1, + isProtected: false, + promotion: 1, + baseAvatarId: 0 + }; + + this.db.equipments.push(equip); + await this.save(); + return; + } + + // Otherwise, add the equipment object directly, but reset it's UID. + equipment.uniqueId = this.db.nextItemUid++; + this.db.equipments.push(equipment); + await this.save(); + + // Send update. + this.sendEquipmentUpdate(); + } + + /** + * Adds the given relic to the player's inventory. + * @param relic Either a `Relic`, or the base id. + */ + public async addRelic(relic: number | Relic) { + // Don't add relics for now until we figure out affix IDs, since the game kinda breaks with + // incorrect ones. + return; + + // If the parameter is a number, add a new equipment with this item ID as base. + /*if (typeof(relic) == "number") { + const rel : Relic = { + tid: relic, + uniqueId: this.db.nextItemUid++, + level: 1, + exp: 1, + isProtected: false, + baseAvatarId: 0, + mainAffixId: 1, + subAffixList: [] + }; + + this.db.relics.push(rel); + return; + } + + // Otherwise, add the equipment object directly, but reset it's UID. + relic.uniqueId = this.db.nextItemUid++; + this.db.relics.push(relic);*/ + } + + /******************************************************************************** + Remove items from the inventory directly. + ********************************************************************************/ + + /** + * Removes the the given number of the given item (virtual or material) with the given ID. + * @param id The item ID. + * @param count The number to remove. + */ + public async removeItem(id: number, count: number) { + const itemData = ItemExcel.fromId(id); + if (!itemData) { + return ; + } + + switch (itemData.ItemType) { + case "Virtual": await this.removeVirtualItem(id, count); break; + case "Material": await this.removeMaterial(id, count); break; + } + } + + /** + * Removes the given amount of the given virtual item from the player's inventory. + * @param id The item id. + * @param count The amount. + */ + public async removeVirtualItem(id: number, count: number) { + await this.addVirtualItem(id, -count); + } + + /** + * Removes the given amount of the given material from the player's inventory. + * @param id The item id. + * @param count The amount. + */ + public async removeMaterial(id: number, count: number) { + await this.addMaterial(id, -count); + } + + /** + * Removes the given equipment player's inventory. + * @param equipment Either an `Equipment`, or the equipment's unique id. + */ + public async removeEquipment(equipment: number | Equipment) { + // Find index to delete. + const toDelete: number = (typeof(equipment) == "number") ? equipment : equipment.uniqueId; + const index = this.db.equipments.findIndex(i => i.uniqueId == toDelete); + + // Delete and save. + this.db.equipments.splice(index, 1); + this.save(); + + // Send update. + this.sendEquipmentUpdate(); + } + + /** + * Removes the given relic player's inventory. + * @param relic Either a `Relic`, or the relic's unique id. + */ + public async removeRelic(relic: number | Relic) { + // Find index to delete. + const toDelete: number = (typeof(relic) == "number") ? relic : relic.uniqueId; + const index = this.db.relics.findIndex(i => i.uniqueId == toDelete); + + // Delete and save. + this.db.relics.splice(index, 1); + this.save(); + + // Send update. + this.sendRelicUpdate() + } + + /******************************************************************************** + Pay items. + ********************************************************************************/ + + /** + * Pay items (virtual items and materials). + * @param items The items to be paid. + * @returns True if paying succeeded, false otherwise. + */ + public async payItems(items: PayItemData[]) : Promise { + // Check if the player has a sufficient amount of all necessary items. + for (const item of items) { + const currentCount = await this.getItemCount(item.id); + if (currentCount < item.count) { + return false; + } + } + + // We have enough of everything - pay. + for (const item of items) { + await this.removeItem(item.id, item.count); + } + + // Send update. + this.sendMaterialUpdate(); + + // Done. + return true; + } + + /******************************************************************************** + Player updating. + ********************************************************************************/ + private sendMaterialUpdate() { + this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ + materialList: this.getMaterialList() + })); + } + private sendEquipmentUpdate() { + this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ + equipmentList: this.getEquipmentList() + })); + } + private sendRelicUpdate() { + this.player.session.send(PlayerSyncScNotify, PlayerSyncScNotify.fromPartial({ + relicList: this.getRelicsList() + })); + } +} \ No newline at end of file diff --git a/src/db/Player.ts b/src/db/Player.ts index c8ee48f..42437eb 100644 --- a/src/db/Player.ts +++ b/src/db/Player.ts @@ -5,6 +5,7 @@ import Account from "./Account"; import Avatar from "./Avatar"; import Database from "./Database"; import { Scene } from "../game/Scene"; +import Inventory from "./Inventory"; const c = new Logger("Player"); export interface LineupI { @@ -49,6 +50,7 @@ interface PlayerI { export default class Player { public readonly uid: number public readonly scene: Scene; + private inventory!: Inventory; private constructor(readonly session: Session, public db: PlayerI) { this.uid = db._id; @@ -105,6 +107,15 @@ export default class Player { this.db.lineup.curIndex = curIndex; } + public async getInventory() : Promise { + // If this players inventory has not been loaded yet, do so now. + if (!this.inventory) { + this.inventory = await Inventory.loadOrCreate(this); + } + + return this.inventory; + } + public static async create(session: Session, uid: number | string): Promise { if (typeof uid == "string") uid = Number(uid); const acc = await Account.fromUID(uid); diff --git a/src/server/kcp/Session.ts b/src/server/kcp/Session.ts index 5a348fe..d5afad7 100644 --- a/src/server/kcp/Session.ts +++ b/src/server/kcp/Session.ts @@ -80,10 +80,15 @@ export default class Session { public async sync() { 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.data) }, + materialList: inventory.getMaterialList(), + equipmentList: inventory.getEquipmentList(), + relicList: inventory.getRelicsList(), basicInfo: this.player.db.basicInfo })); diff --git a/src/server/packets/DoGachaCsReq.ts b/src/server/packets/DoGachaCsReq.ts index 05174e7..2798719 100644 --- a/src/server/packets/DoGachaCsReq.ts +++ b/src/server/packets/DoGachaCsReq.ts @@ -1,4 +1,5 @@ import { DoGachaCsReq, DoGachaScRsp, GachaItem, Item, ItemList } from "../../data/proto/StarRail"; +import { PayItemData } from "../../db/Inventory"; import Banners from "../../util/Banner"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; @@ -8,6 +9,17 @@ export default async function handle(session: Session, packet: Packet) { const body = packet.body as DoGachaCsReq; const banner = Banners.config.find(banner => banner.gachaId === body.gachaId)!; const combined = banner.rateUpItems4.concat(banner.rateUpItems5) + + // Pay currency. + const inventory = await session.player.getInventory(); + const success = await inventory.payItems([{ id: banner.costItemId, count: body.gachaNum } as PayItemData]); + + if (!success) { + session.send(DoGachaScRsp, { + retcode: 1 + } as DoGachaScRsp); + } + //bad gachaing but whatever.... //TODO: pity system, proper logic for(let i = 0; i < body.gachaNum; i++){ diff --git a/src/server/packets/GetBagCsReq.ts b/src/server/packets/GetBagCsReq.ts index 8af4f24..1693c24 100644 --- a/src/server/packets/GetBagCsReq.ts +++ b/src/server/packets/GetBagCsReq.ts @@ -1,12 +1,14 @@ -import { GetBagScRsp } from "../../data/proto/StarRail"; +import { Equipment, GetBagScRsp, Material, Relic } from "../../data/proto/StarRail"; import Packet from "../kcp/Packet"; import Session from "../kcp/Session"; export default async function handle(session: Session, packet: Packet) { + const inventory = await session.player.getInventory(); + session.send(GetBagScRsp, { - equipmentList: [], - materialList: [], - relicList: [], + equipmentList: inventory.getEquipmentList(), + materialList: inventory.getMaterialList(), + relicList: inventory.getRelicsList(), retcode: 0, rogueItemList: [], waitDelResourceList: [] diff --git a/src/util/Banner.ts b/src/util/Banner.ts index 91e6454..767512c 100644 --- a/src/util/Banner.ts +++ b/src/util/Banner.ts @@ -33,7 +33,7 @@ export default class Banners { rateUpItems5: [ 1102 ], - costItemId: -1 //unused for now + costItemId: 101 // Star Rail Pass } as Banner ]; diff --git a/src/util/excel/ItemExcel.ts b/src/util/excel/ItemExcel.ts new file mode 100644 index 0000000..4f78af5 --- /dev/null +++ b/src/util/excel/ItemExcel.ts @@ -0,0 +1,20 @@ +import _ItemExcelTable from "../../data/excel/ItemExcelTable.json"; +type ItemExcelTableEntry = typeof _ItemExcelTable[keyof typeof _ItemExcelTable] +const ItemExcelTable = _ItemExcelTable as { [key: string]: ItemExcelTableEntry }; + +export default class ItemExcel { + private constructor() { + } + + public static all() : ItemExcelTableEntry[] { + return Object.values(ItemExcelTable); + } + + public static fromId(id: number) : ItemExcelTableEntry { + return ItemExcelTable[id]; + } + + public static fromIds(ids: number[]): ItemExcelTableEntry[] { + return ids.map(id => ItemExcel.fromId(id)); + } +} \ No newline at end of file