diff --git a/database/inventory/inventory_data.py b/database/inventory/inventory_data.py index 69f4007..e4d6d04 100644 --- a/database/inventory/inventory_data.py +++ b/database/inventory/inventory_data.py @@ -11,9 +11,9 @@ from pydantic import BaseModel from rail_proto.lib import ( Equipment, Relic, - RelicAffix, BattleRelic, - RelicAffix + RelicAffix, + PlayerSyncScNotify ) items_collection = get_collection("items") @@ -124,6 +124,18 @@ class InventoryData(BaseDatabaseData): ] ) return proto + + def RelicSyncProto(self): + proto = PlayerSyncScNotify( + relic_list=[self.ToProto()] + ) + return proto + + def LightconeSyncProto(self): + proto = PlayerSyncScNotify( + equipment_list=[self.ToProto()] + ) + return proto def save_item(self): item_data = self.model_dump() diff --git a/game_server/game/player/player_manager.py b/game_server/game/player/player_manager.py index 9fc1468..a3495e5 100644 --- a/game_server/game/player/player_manager.py +++ b/game_server/game/player/player_manager.py @@ -10,7 +10,7 @@ from rail_proto.lib import ( PlayerSyncScNotify, AvatarSync ) -from pymongo import UpdateOne +from pymongo import UpdateOne, DeleteMany from utils.logger import Error @@ -82,12 +82,14 @@ class PlayerManager(BaseModel): return None def add_lineup(self,avatar_ids: list[int],index=0,name=""): - LineupData( + lineup = LineupData( uid=self.data.uid, index=index, name=name, avatar_list=avatar_ids ).add_lineup() + if lineup: + self.lineup_manager[lineup.index] = lineup def add_lightcone(self,lightcone_id: int, rank=1) -> Optional[InventoryData]: item = InventoryData( @@ -110,6 +112,25 @@ class PlayerManager(BaseModel): return item return None + async def unequip_items(self,avatar_id): + avatar_data = self.avatar_manager.get(avatar_id) + if not avatar_data:return + item_data = self.inventory_manager.get(avatar_data.lightcone_id) + if not item_data:return + item_data.equip_avatar = 0 + + match item_data.main_type: + case 1: + avatar_data.lightcone_id = 0 + case 2: + avatar_data.relic_ids[str(item_data.sub_type)] = 0 + return + + async def remove_items(self,count,unique_id): + item_data = self.inventory_manager.get(unique_id) + if not item_data:return + if count == 0: self.inventory_manager.pop(unique_id) + return def PlayerSyncProto(self) -> PlayerSyncScNotify: avatars = [] @@ -135,6 +156,33 @@ class PlayerManager(BaseModel): basic_info=self.data.ToProto() ) + def SrToolAvatarSync(self): + avatars = [] + for avatar_id, avatar in self.avatar_manager.items(): + avatars.append(avatar.ToProto()) + + return PlayerSyncScNotify( + avatar_sync= + AvatarSync( + avatar_list=avatars + ) + ) + + def SrToolItemsSync(self): + equipment = [] + for unique_id, item in self.inventory_manager.items(): + if item.main_type == 1: + equipment.append(item.ToProto()) + + relics = [] + for unique_id, item in self.inventory_manager.items(): + if item.main_type == 2: + relics.append(item.ToProto()) + return PlayerSyncScNotify( + equipment_list=equipment, + relic_list=relics + ) + def save_player_data_bulk(self): try: player_data = self.data.model_dump() @@ -168,11 +216,20 @@ class PlayerManager(BaseModel): def save_all_items_bulk(self): operations = [] + current_unique_ids = set() + for item in self.inventory_manager.values(): item_data = item.model_dump() item_data["uid"] = item.uid query = {"uid": item.uid, "unique_id": item.unique_id} operations.append(UpdateOne(query, {"$set": item_data}, upsert=True)) + current_unique_ids.add(item.unique_id) + + delete_query = { + "unique_id": {"$nin": list(current_unique_ids)}, + "uid": self.data.uid + } + operations.append(DeleteMany(delete_query)) if operations: items_collection.bulk_write(operations) \ No newline at end of file diff --git a/game_server/handlers/DressAvatarCsReq.py b/game_server/handlers/DressAvatarCsReq.py index 0981540..40d13da 100644 --- a/game_server/handlers/DressAvatarCsReq.py +++ b/game_server/handlers/DressAvatarCsReq.py @@ -18,21 +18,17 @@ async def handle(session: PlayerSession, msg: DressAvatarCsReq) -> betterproto.M target_equipment = session.player.inventory_manager.get(target_avatar.lightcone_id) target_avatar.lightcone_id, previous_avatar.lightcone_id = previous_avatar.lightcone_id, target_avatar.lightcone_id target_equipment.equip_avatar, equipment.equip_avatar = previous_avatar.avatar_id, target_avatar.avatar_id - #target_equipment.save_item() else: previous_avatar.lightcone_id = 0 equipment.equip_avatar = target_avatar.avatar_id target_avatar.lightcone_id = equipment.unique_id - #previous_avatar.save_avatar() else: if target_avatar.lightcone_id > 0: previous_equipment = session.player.inventory_manager.get(target_avatar.lightcone_id) previous_equipment.equip_avatar = 0 - #previous_equipment.save_item() equipment.equip_avatar = target_avatar.avatar_id target_avatar.lightcone_id = equipment.unique_id - #equipment.save_item() - #target_avatar.save_avatar() - await session.notify(session.player.PlayerSyncProto()) - return DressAvatarScRsp(retcode=0) \ No newline at end of file + + session.pending_notify(session.player.PlayerSyncProto()) + return DressAvatarScRsp() \ No newline at end of file diff --git a/game_server/handlers/DressRelicAvatarCsReq.py b/game_server/handlers/DressRelicAvatarCsReq.py index faf8bcb..aaa90ca 100644 --- a/game_server/handlers/DressRelicAvatarCsReq.py +++ b/game_server/handlers/DressRelicAvatarCsReq.py @@ -33,41 +33,30 @@ async def handle(session: PlayerSession, msg: DressRelicAvatarCsReq) -> betterpr target_avatar.relic_ids[relic_sub_type] = previous_relic.unique_id previous_relic.equip_avatar = target_avatar.avatar_id - #current_relic.save_item() - #previous_relic.save_item() - elif previous_relic_id > 0: previous_relic = session.player.inventory_manager.get(previous_relic_id) if previous_relic: previous_relic.equip_avatar = 0 - #previous_relic.save_item() previous_avatar.relic_ids[relic_sub_type] = 0 elif current_relic_id > 0: current_relic = session.player.inventory_manager.get(current_relic_id) if current_relic: current_relic.equip_avatar = 0 - #current_relic.save_item() target_avatar.relic_ids[relic_sub_type] = 0 target_avatar.relic_ids[relic_sub_type] = relic.unique_id relic.equip_avatar = target_avatar.avatar_id - #relic.save_item() - #previous_avatar.save_avatar() else: if current_relic_id > 0: current_relic = session.player.inventory_manager.get(current_relic_id) if current_relic: current_relic.equip_avatar = 0 - #current_relic.save_item() target_avatar.relic_ids[relic_sub_type] = relic.unique_id relic.equip_avatar = target_avatar.avatar_id - #relic.save_item() - #target_avatar.save_avatar() - - await session.notify(session.player.PlayerSyncProto()) + session.pending_notify(session.player.PlayerSyncProto()) - return DressRelicAvatarScRsp(retcode=0) + return DressRelicAvatarScRsp() diff --git a/game_server/handlers/GetBigDataAllRecommendCsReq.py b/game_server/handlers/GetBigDataAllRecommendCsReq.py new file mode 100644 index 0000000..739e0e1 --- /dev/null +++ b/game_server/handlers/GetBigDataAllRecommendCsReq.py @@ -0,0 +1,33 @@ +import betterproto +from game_server.net.session import PlayerSession +from rail_proto.lib import ( + GetBigDataAllRecommendCsReq, + GetBigDataAllRecommendScRsp, + PIIIPHEFDJO, + MKJALMKMPGL, + OFNGPLJKLOJ, + KNNFPFKCABE +) + +async def handle(session: PlayerSession, msg: GetBigDataAllRecommendCsReq) -> betterproto.Message: + unk1 = PIIIPHEFDJO() + unk2 = MKJALMKMPGL() + list1 = [] + for _,avatar_data in session.player.avatar_manager.items(): + id = avatar_data.avatar_id + list1.append(id) + + unk2unk2 = OFNGPLJKLOJ() + unk2unk2.avatar_id = id + unk2.bfdmginboib.append(unk2unk2) + unk1.apfecoopnkn.append( + KNNFPFKCABE( + avatar_id_list=list1 + ) + ) + + return GetBigDataAllRecommendScRsp( + big_data_recommend_type=msg.big_data_recommend_type, + dklbnbdpmpo=unk1, + pfopjpjkklk=unk2, + ) \ No newline at end of file diff --git a/game_server/handlers/GetBigDataRecommendCsReq.py b/game_server/handlers/GetBigDataRecommendCsReq.py new file mode 100644 index 0000000..fa8ab37 --- /dev/null +++ b/game_server/handlers/GetBigDataRecommendCsReq.py @@ -0,0 +1,13 @@ +import betterproto +from game_server.net.session import PlayerSession +from rail_proto.lib import ( + GetBigDataRecommendCsReq, + GetBigDataRecommendScRsp +) + +async def handle(session: PlayerSession, msg: GetBigDataRecommendCsReq) -> betterproto.Message: + return GetBigDataRecommendScRsp( + big_data_recommend_type=msg.big_data_recommend_type, + has_recommand=True, + equip_avatar=msg.equip_avatar + ) \ No newline at end of file diff --git a/game_server/main.py b/game_server/main.py index afc0b72..2bb056f 100644 --- a/game_server/main.py +++ b/game_server/main.py @@ -1,4 +1,6 @@ import asyncio +from threading import Thread +from game_server.srtools.server import run_flask from game_server.net.gateway import KCPGateway from utils.config import Config from game_server.resource import ResourceManager @@ -6,7 +8,7 @@ from game_server.resource import ResourceManager def fn_main(): try: ResourceManager.instance().load_resources() - asyncio.run(KCPGateway.new(Config.GameServer.IP,Config.GameServer.Port)) + Thread(target=run_flask, args=(Config.SRToolsServer.IP, Config.SRToolsServer.Port), daemon=True).start() + asyncio.run(KCPGateway.new(Config.GameServer.IP, Config.GameServer.Port)) except Exception as e: - print(f"Error: {e}") - + print(f"Error: {e}") \ No newline at end of file diff --git a/game_server/net/gateway.py b/game_server/net/gateway.py index beb261b..095a6ee 100644 --- a/game_server/net/gateway.py +++ b/game_server/net/gateway.py @@ -5,6 +5,7 @@ from game_server.net.packet import NetOperation from game_server.net.session import PlayerSession from utils.logger import Info,Warn,Debug from database.mongodb import get_database +from game_server.srtools.gateway import set_gateway_instance class KCPGateway(asyncio.DatagramProtocol): @@ -17,6 +18,12 @@ class KCPGateway(asyncio.DatagramProtocol): self.timeout_check_interval = 5 self.save_duration = 60 + def get_active_connection(self,uid) -> PlayerSession: + for key, session in self.sessions.items(): + if session.player.data.uid == uid: + return session + return None + async def check_sessions_timeout(self): while self.running: for conv_id, session in list(self.sessions.items()): @@ -134,6 +141,8 @@ class KCPGateway(asyncio.DatagramProtocol): lambda: KCPGateway(db), local_addr=(host, port) ) + set_gateway_instance(protocol) + try: await protocol.shutdown_event.wait() except asyncio.CancelledError: diff --git a/game_server/srtools/gateway.py b/game_server/srtools/gateway.py new file mode 100644 index 0000000..9497592 --- /dev/null +++ b/game_server/srtools/gateway.py @@ -0,0 +1,8 @@ +gateway_instance = None + +def set_gateway_instance(instance): + global gateway_instance + gateway_instance = instance + +def get_gateway_instance(): + return gateway_instance diff --git a/game_server/srtools/models/srtools_data.py b/game_server/srtools/models/srtools_data.py new file mode 100644 index 0000000..9580320 --- /dev/null +++ b/game_server/srtools/models/srtools_data.py @@ -0,0 +1,74 @@ +from pydantic import BaseModel +from typing import Optional + +class SRToolData(BaseModel): + class RelicSubAffixData(BaseModel): + sub_affix_id: int + count: int + step: int + + class RelicData(BaseModel): + level: int + relic_id: int + relic_set_id: int + main_affix_id: int + sub_affixes: Optional[list["SRToolData.RelicSubAffixData"]] = None + internal_uid: int + equip_avatar: int + + class LightconeData(BaseModel): + level: int + internal_uid: int + equip_avatar: int + item_id: int + rank: int + promotion: int + + class AvatarData(BaseModel): + avatar_id: int + data: Optional["SRToolData.AvatarInnerData"] = None + level: int + promotion: int + techniques: Optional[list[int]] = [] + sp_value: int + sp_max: int + + class AvatarInnerData(BaseModel): + rank: int + skills: dict[str,int] + + class DynamicKey(BaseModel): + key: Optional[str] = "" + value: int + + class BlessingData(BaseModel): + level: int + id: int + dynamic_key: Optional["SRToolData.DynamicKey"] = None + + class MonsterData(BaseModel): + level: int + monster_id: int + amount: int + + class BattleConfigData(BaseModel): + battle_type: Optional[str] = "" + blessings: Optional[list["SRToolData.BlessingData"]] = None + custom_stats: Optional[list["SRToolData.RelicSubAffixData"]] = None + cycle_count: int + monsters: Optional[list["SRToolData.MonsterData"]] = None + path_resonance_id: int + stage_id: int + + relics: Optional[list[RelicData]] = None + lightcones: Optional[list[LightconeData]] = None + avatars: Optional[dict[int,AvatarData]] = {} + +class SRToolDataReq(BaseModel): + data: Optional[SRToolData] = None + username: Optional[str] = "" + password: Optional[str] = "" + +class SRToolDataRsp(BaseModel): + status: int + message: Optional[str] = "" \ No newline at end of file diff --git a/game_server/srtools/server.py b/game_server/srtools/server.py new file mode 100644 index 0000000..47d971b --- /dev/null +++ b/game_server/srtools/server.py @@ -0,0 +1,119 @@ + +import logging +from flask import Flask, request, jsonify +from flask_cors import CORS +import traceback +import asyncio + +from database.inventory.inventory_data import SubAffix +from game_server.game.player.player_manager import PlayerManager +from game_server.net.session import PlayerSession +from game_server.srtools.models.srtools_data import SRToolDataRsp,SRToolDataReq,SRToolData +from database.account.account_data import find_account_by_name +from utils.logger import Info +from game_server.srtools.gateway import get_gateway_instance +flask_app = Flask(__name__) +log = logging.getLogger('werkzeug') +log.setLevel(logging.ERROR) +CORS(flask_app, resources={r"/*": {"origins": "https://srtools.pages.dev"}}) + + +async def ClearInventory(conn: PlayerSession): + # un equip all lightcones and relic after that remove it + for _, items in list(conn.player.inventory_manager.items()): + if items.equip_avatar != 0 and items.main_type in (1, 2): + await conn.player.unequip_items(items.equip_avatar) + if items.main_type == 1: + await conn.notify(items.LightconeSyncProto()) + if items.main_type == 2: + await conn.notify(items.RelicSyncProto()) + await conn.player.remove_items(0, items.unique_id) + +async def AddAllData(player: PlayerManager, data: SRToolData): + for key, avatar_data in data.avatars.items(): + avatar = player.avatar_manager.get(avatar_data.avatar_id) + if avatar_data.avatar_id >= 8000:continue + if not avatar: + avatar = player.add_avatar(avatar_data.avatar_id) + if avatar: + player.avatar_manager[avatar_data.avatar_id] = avatar + + avatar.level = avatar_data.level + avatar.promotion = avatar_data.promotion + avatar.rank = avatar_data.data.rank if avatar_data.data else 0 + avatar.skills = avatar_data.data.skills + + for lightcone in data.lightcones: + if lightcone.equip_avatar > 8000:continue + lightcone_data = player.add_lightcone( + lightcone.item_id, + lightcone.rank + ) + if lightcone_data: + if lightcone.equip_avatar > 0: + player.avatar_manager[lightcone.equip_avatar].lightcone_id = lightcone_data.unique_id + lightcone_data.equip_avatar = lightcone.equip_avatar + player.inventory_manager[lightcone_data.unique_id] = lightcone_data + + for relic in data.relics: + if relic.equip_avatar > 8000:continue + relic_data = player.add_relic( + relic_id=relic.relic_id, + main_affix=relic.main_affix_id, + sub_affix=[ + SubAffix( + id=affix.sub_affix_id, + count=affix.count, + step=affix.step + ) + for affix in relic.sub_affixes + ] + ) + if relic_data: + if relic.equip_avatar > 0: + player.avatar_manager[relic.equip_avatar].relic_ids[str(relic_data.sub_type)] = relic_data.unique_id + relic_data.equip_avatar = relic.equip_avatar + player.inventory_manager[relic_data.unique_id] = relic_data + + +@flask_app.route("/srtools", methods=["POST"]) +async def srtools_handler(): + body = request.get_json() + try: + req = SRToolDataReq(**body) + except Exception: + return jsonify({"message": "Invalid input", "status": 400}), 400 + + rsp = SRToolDataRsp(message="OK", status=200) + + account = find_account_by_name(req.username) + if not account: + rsp.message = "Invalid account" + rsp.status = 400 + return jsonify(rsp.model_dump()), 401 + + if req.data is None: + return jsonify(rsp.model_dump()), 200 + + gateway = get_gateway_instance() + conn : PlayerSession = gateway.get_active_connection(account.id) + if not conn: + rsp.message = "Player Not Online" + rsp.status = 401 + return jsonify(rsp.model_dump()), 401 + + try: + await ClearInventory(conn) + await AddAllData(conn.player,req.data) + conn.pending_notify(conn.player.SrToolItemsSync()) + conn.pending_notify(conn.player.SrToolAvatarSync()) + except: + rsp.message = "Internal Server Error" + rsp.status = 500 + print(traceback.print_exc()) + return jsonify(rsp.model_dump()), 500 + return jsonify(rsp.model_dump()), 200 + +def run_flask(host, port): + Info(f"SRTools Server Starting on {host}:{port}") + flask_app.run(host=host, port=port) diff --git a/utils/config.py b/utils/config.py index a3bd6cb..b195b63 100644 --- a/utils/config.py +++ b/utils/config.py @@ -15,12 +15,14 @@ class ConfigData: LogLevel: str GameServer: ServerConfig SDKServer: ServerConfig + SRToolsServer: ServerConfig RegionName: str def write_default_config(): config = ConfigData( LogLevel="INFO", GameServer=ServerConfig(IP="127.0.0.1", Port=23301), SDKServer=ServerConfig(IP="127.0.0.1", Port=21000), + SRToolsServer=ServerConfig(IP="127.0.0.1", Port=25000), RegionName="NeonSR", ) with open("Config.json", "w") as f: