init
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/.vscode
|
||||||
|
*.pyc
|
||||||
|
logs/latest.log
|
||||||
|
*.rs
|
||||||
|
/build
|
||||||
|
__pycache__/
|
||||||
|
.env
|
||||||
|
/resources
|
||||||
|
*.proto
|
||||||
|
*.txt
|
||||||
|
/proto
|
||||||
|
.idea/
|
||||||
|
/logs
|
||||||
39
database/account/account_data.py
Normal file
39
database/account/account_data.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
from database.mongodb import get_collection
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from utils.crypto import generate_combo_token
|
||||||
|
|
||||||
|
class AccountModel(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
token: str
|
||||||
|
|
||||||
|
account_collection = get_collection("accounts")
|
||||||
|
|
||||||
|
def find_account_by_uid(uid: int) -> Optional[AccountModel]:
|
||||||
|
account_data = account_collection.find_one({"_id": uid})
|
||||||
|
if account_data:
|
||||||
|
account_data["id"] = int(account_data["_id"])
|
||||||
|
del account_data["_id"]
|
||||||
|
return AccountModel(**account_data)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_account_by_name(name: str) -> Optional[AccountModel]:
|
||||||
|
account_data = account_collection.find_one({"username": name})
|
||||||
|
if account_data:
|
||||||
|
account_data["id"] = int(account_data["_id"])
|
||||||
|
del account_data["_id"]
|
||||||
|
return AccountModel(**account_data)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_new_account(username: str) -> AccountModel:
|
||||||
|
last_account = account_collection.find_one(sort=[("_id", -1)])
|
||||||
|
uid = (last_account["_id"] + 1) if last_account else 1001
|
||||||
|
token = generate_combo_token(str(uid))
|
||||||
|
new_account = {
|
||||||
|
"_id": uid,
|
||||||
|
"username": username,
|
||||||
|
"token": token
|
||||||
|
}
|
||||||
|
account_collection.insert_one(new_account)
|
||||||
|
return AccountModel(id=uid, username=username, token=new_account["token"])
|
||||||
142
database/avatar/avatar_data.py
Normal file
142
database/avatar/avatar_data.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
from database.mongodb import get_collection
|
||||||
|
from typing import Optional
|
||||||
|
from database.base_database_data import BaseDatabaseData
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.avatar_config import AvatarConfig
|
||||||
|
from typing import List
|
||||||
|
from rail_proto.lib import (
|
||||||
|
Avatar,
|
||||||
|
AvatarSkillTree,
|
||||||
|
BattleAvatar,
|
||||||
|
AvatarType,
|
||||||
|
SpBarInfo,
|
||||||
|
AvatarSkillTree,
|
||||||
|
BattleEquipment,
|
||||||
|
EquipRelic
|
||||||
|
)
|
||||||
|
|
||||||
|
avatars_collection = get_collection("avatars")
|
||||||
|
|
||||||
|
class AvatarData(BaseDatabaseData):
|
||||||
|
avatar_id: int
|
||||||
|
level: int = 80
|
||||||
|
exp: int = 0
|
||||||
|
promotion: int = 6
|
||||||
|
rank: int = 6
|
||||||
|
lightcone_id: int = 0
|
||||||
|
relic_ids: dict[str,int] = {}
|
||||||
|
skills: dict[str,int] = {}
|
||||||
|
current_hp: int = 10000
|
||||||
|
current_sp: int = 0
|
||||||
|
|
||||||
|
def add_avatar(self) -> "AvatarData":
|
||||||
|
get_avatar = avatars_collection.find_one({"uid": self.uid, "avatar_id":self.avatar_id})
|
||||||
|
if get_avatar:
|
||||||
|
return False
|
||||||
|
|
||||||
|
avatar = ResourceManager.instance().find_by_index(AvatarConfig, self.avatar_id)
|
||||||
|
if not avatar:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.avatar_id=avatar.AvatarID
|
||||||
|
|
||||||
|
for skill_id,skill_level in avatar.AvatarSkills.items():
|
||||||
|
self.skills[str(skill_id)] = skill_level
|
||||||
|
|
||||||
|
avatar_data = self.model_dump()
|
||||||
|
avatars_collection.insert_one(avatar_data)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def ToProto(self) -> Avatar:
|
||||||
|
return Avatar(
|
||||||
|
base_avatar_id=self.avatar_id,
|
||||||
|
level=self.level,
|
||||||
|
exp=self.exp,
|
||||||
|
promotion=self.promotion,
|
||||||
|
rank=self.rank,
|
||||||
|
skilltree_list=[
|
||||||
|
AvatarSkillTree(
|
||||||
|
point_id=int(skill_id),
|
||||||
|
level=skill_level
|
||||||
|
)
|
||||||
|
for skill_id,skill_level in self.skills.items()
|
||||||
|
],
|
||||||
|
equipment_unique_id=self.lightcone_id,
|
||||||
|
equip_relic_list=[
|
||||||
|
EquipRelic(
|
||||||
|
relic_unique_id=relic_id,
|
||||||
|
type=int(type)
|
||||||
|
)
|
||||||
|
for type,relic_id in self.relic_ids.items()
|
||||||
|
],
|
||||||
|
has_taken_promotion_reward_list=[1,2,3,4,5,6],
|
||||||
|
)
|
||||||
|
|
||||||
|
def ToBattleProto(self,index,session):
|
||||||
|
get_equipment = None
|
||||||
|
if self.lightcone_id > 0:
|
||||||
|
get_equipment = session.player.inventory_manager.get(self.lightcone_id)
|
||||||
|
|
||||||
|
return BattleAvatar(
|
||||||
|
id=self.avatar_id,
|
||||||
|
index=index,
|
||||||
|
level=self.level,
|
||||||
|
promotion=self.promotion,
|
||||||
|
rank=self.rank,
|
||||||
|
hp=self.current_hp,
|
||||||
|
avatar_type=AvatarType.AVATAR_FORMAL_TYPE.value,
|
||||||
|
sp_bar=SpBarInfo(
|
||||||
|
cur_sp=5000,
|
||||||
|
max_sp=10000
|
||||||
|
),
|
||||||
|
skilltree_list=[
|
||||||
|
AvatarSkillTree(
|
||||||
|
point_id=int(skill_id),
|
||||||
|
level=level
|
||||||
|
)
|
||||||
|
for skill_id,level in self.skills.items()
|
||||||
|
],
|
||||||
|
equipment_list=[
|
||||||
|
BattleEquipment(
|
||||||
|
id=get_equipment.item_id if get_equipment else 0,
|
||||||
|
level=get_equipment.level if get_equipment else 0,
|
||||||
|
promotion=get_equipment.promotion if get_equipment else 0,
|
||||||
|
rank=get_equipment.rank if get_equipment else 0
|
||||||
|
)
|
||||||
|
],
|
||||||
|
relic_list = [
|
||||||
|
session.player.inventory_manager.get(relic_id).RelicBattleProto()
|
||||||
|
for type, relic_id in self.relic_ids.items()
|
||||||
|
if relic_id > 0
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_avatar(self):
|
||||||
|
avatar_data = self.model_dump()
|
||||||
|
avatar_data["uid"] = self.uid
|
||||||
|
query = {"uid": self.uid, "avatar_id": self.avatar_id}
|
||||||
|
avatars_collection.update_one(query, {"$set": avatar_data})
|
||||||
|
|
||||||
|
|
||||||
|
def find_avatar_by_avatar_id(uid: int, avatar_id: int) -> Optional[AvatarData]:
|
||||||
|
avatar_data = avatars_collection.find_one({"uid": uid, "avatar_id":avatar_id})
|
||||||
|
if avatar_data:
|
||||||
|
del avatar_data["_id"]
|
||||||
|
return AvatarData(**avatar_data)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_avatars_by_uid(uid: int) -> List[AvatarData]:
|
||||||
|
avatars_data = avatars_collection.find({"uid": uid})
|
||||||
|
result = []
|
||||||
|
for avatar in avatars_data:
|
||||||
|
del avatar["_id"]
|
||||||
|
result.append(AvatarData(**avatar))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
4
database/base_database_data.py
Normal file
4
database/base_database_data.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class BaseDatabaseData(BaseModel):
|
||||||
|
uid: int = 0
|
||||||
159
database/inventory/inventory_data.py
Normal file
159
database/inventory/inventory_data.py
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
from database.mongodb import get_collection
|
||||||
|
from database.base_database_data import BaseDatabaseData
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.relic_main_affix_config import RelicMainAffixConfigData
|
||||||
|
from game_server.resource.configdb.equipment_config import EquipmentConfig
|
||||||
|
from game_server.resource.configdb.relic_config import RelicConfigData
|
||||||
|
from game_server.game.items.relic_manager import RelicManager
|
||||||
|
from game_server.game.enums.avatar.relic_type import RelicTypeEnum
|
||||||
|
from typing import List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from rail_proto.lib import (
|
||||||
|
Equipment,
|
||||||
|
Relic,
|
||||||
|
RelicAffix,
|
||||||
|
BattleRelic,
|
||||||
|
RelicAffix
|
||||||
|
)
|
||||||
|
|
||||||
|
items_collection = get_collection("items")
|
||||||
|
|
||||||
|
class SubAffix(BaseModel):
|
||||||
|
id: int
|
||||||
|
count: int
|
||||||
|
step: int
|
||||||
|
|
||||||
|
class InventoryData(BaseDatabaseData):
|
||||||
|
item_id: int
|
||||||
|
count: int = 1
|
||||||
|
level: int = 1
|
||||||
|
exp: int = 0
|
||||||
|
promotion: int = 0
|
||||||
|
rank: int = 0
|
||||||
|
locked: bool = False
|
||||||
|
discarded: bool = False
|
||||||
|
main_affix: int = 0
|
||||||
|
sub_affix: list[SubAffix] = []
|
||||||
|
equip_avatar: int = 0
|
||||||
|
unique_id: int = 0
|
||||||
|
main_type: int = 0
|
||||||
|
sub_type: int = 0
|
||||||
|
|
||||||
|
def add_lightcone(self):
|
||||||
|
last_item = items_collection.find_one({"uid": self.uid},sort=[("unique_id", -1)])
|
||||||
|
unique_id = last_item["unique_id"] + 1 if last_item else 1
|
||||||
|
equipment = ResourceManager.instance().find_by_index(EquipmentConfig, self.item_id)
|
||||||
|
if not equipment:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.level = 80
|
||||||
|
self.promotion = 6
|
||||||
|
self.main_type = 1
|
||||||
|
self.unique_id = unique_id
|
||||||
|
|
||||||
|
lightcone_data = self.model_dump()
|
||||||
|
items_collection.insert_one(lightcone_data)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_relic(self):
|
||||||
|
last_item = items_collection.find_one({"uid": self.uid}, sort=[("unique_id", -1)])
|
||||||
|
unique_id = last_item["unique_id"] + 1 if last_item else 1
|
||||||
|
|
||||||
|
relic = ResourceManager.instance().find_by_index(RelicConfigData, self.item_id)
|
||||||
|
if not relic:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.level = 15
|
||||||
|
self.main_type = 2
|
||||||
|
self.sub_type = RelicTypeEnum[relic.Type].value
|
||||||
|
|
||||||
|
relic_manager = RelicManager()
|
||||||
|
main_stat = ""
|
||||||
|
get_main_affix = None
|
||||||
|
|
||||||
|
if self.main_affix > 0:
|
||||||
|
relics = ResourceManager.instance().find_by_index(RelicConfigData, self.item_id)
|
||||||
|
main_affix_group = ResourceManager.instance().find_all_by_index(RelicMainAffixConfigData, relics.MainAffixGroup)
|
||||||
|
main_stat = next(
|
||||||
|
(affix.Property for affix in main_affix_group if affix.AffixID == int(self.main_affix))
|
||||||
|
)
|
||||||
|
elif self.main_affix == 0:
|
||||||
|
get_main_affix = relic_manager.GetRandomRelicMainAffix(self.item_id)
|
||||||
|
self.main_affix = get_main_affix.AffixID
|
||||||
|
|
||||||
|
if not main_stat and get_main_affix:
|
||||||
|
main_stat = get_main_affix.Property
|
||||||
|
|
||||||
|
if not self.sub_affix:
|
||||||
|
self.sub_affix = relic_manager.GetRandomRelicSubAffix(self.item_id, main_stat, 4)
|
||||||
|
|
||||||
|
if self.sub_affix and len(self.sub_affix) < 4:
|
||||||
|
self.sub_affix.extend(relic_manager.GetRandomRelicSubAffix(self.item_id, main_stat, 4 - len(self.sub_affix), self.sub_affix))
|
||||||
|
|
||||||
|
self.unique_id = unique_id
|
||||||
|
|
||||||
|
relic_data = self.model_dump()
|
||||||
|
items_collection.insert_one(relic_data)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def ToProto(self):
|
||||||
|
if self.main_type == 1:
|
||||||
|
proto = Equipment(
|
||||||
|
tid=self.item_id,
|
||||||
|
level=self.level,
|
||||||
|
rank=self.rank,
|
||||||
|
promotion=self.promotion,
|
||||||
|
dress_avatar_id=self.equip_avatar,
|
||||||
|
unique_id=self.unique_id,
|
||||||
|
)
|
||||||
|
if self.main_type == 2:
|
||||||
|
proto = Relic(
|
||||||
|
tid=self.item_id,
|
||||||
|
level=self.level,
|
||||||
|
dress_avatar_id=self.equip_avatar,
|
||||||
|
unique_id=self.unique_id,
|
||||||
|
main_affix_id=self.main_affix,
|
||||||
|
sub_affix_list=[
|
||||||
|
RelicAffix(
|
||||||
|
affix_id=affix.id,
|
||||||
|
cnt=affix.count,
|
||||||
|
step=affix.step
|
||||||
|
)
|
||||||
|
for affix in self.sub_affix
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return proto
|
||||||
|
|
||||||
|
def save_item(self):
|
||||||
|
item_data = self.model_dump()
|
||||||
|
item_data["uid"] = self.uid
|
||||||
|
query = {"uid": self.uid, "unique_id": self.unique_id}
|
||||||
|
items_collection.update_one(query, {"$set": item_data})
|
||||||
|
|
||||||
|
def RelicBattleProto(self):
|
||||||
|
return BattleRelic(
|
||||||
|
id=self.item_id,
|
||||||
|
level=self.level,
|
||||||
|
main_affix_id=self.main_affix,
|
||||||
|
sub_affix_list=[
|
||||||
|
RelicAffix(
|
||||||
|
affix_id=affix.id,
|
||||||
|
cnt=affix.count,
|
||||||
|
step=affix.step
|
||||||
|
)
|
||||||
|
for affix in self.sub_affix
|
||||||
|
],
|
||||||
|
unique_id=self.unique_id,
|
||||||
|
type=self.sub_type
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_items_by_uid(uid: int) -> List[InventoryData]:
|
||||||
|
items_data = items_collection.find({"uid": uid})
|
||||||
|
result = []
|
||||||
|
for items in items_data:
|
||||||
|
del items["_id"]
|
||||||
|
result.append(InventoryData(**items))
|
||||||
|
return result
|
||||||
67
database/lineup/lineup_data.py
Normal file
67
database/lineup/lineup_data.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from database.mongodb import get_collection
|
||||||
|
from database.base_database_data import BaseDatabaseData
|
||||||
|
from typing import List
|
||||||
|
from rail_proto.lib import (
|
||||||
|
LineupInfo,
|
||||||
|
ExtraLineupType,
|
||||||
|
LineupAvatar,
|
||||||
|
SpBarInfo,
|
||||||
|
AvatarType
|
||||||
|
)
|
||||||
|
|
||||||
|
lineups_collection = get_collection("lineups")
|
||||||
|
|
||||||
|
class LineupData(BaseDatabaseData):
|
||||||
|
name: str = ""
|
||||||
|
index: int = 0
|
||||||
|
mp: int = 5
|
||||||
|
extra_lineup: ExtraLineupType = ExtraLineupType.LINEUP_NONE
|
||||||
|
leader_slot: int = 0
|
||||||
|
avatar_list: list[int] = []
|
||||||
|
|
||||||
|
def ToProto(self) -> LineupInfo:
|
||||||
|
return LineupInfo(
|
||||||
|
name=self.name,
|
||||||
|
index=self.index,
|
||||||
|
mp=self.mp,
|
||||||
|
max_mp=5,
|
||||||
|
extra_lineup_type=self.extra_lineup,
|
||||||
|
leader_slot=self.leader_slot,
|
||||||
|
avatar_list=[
|
||||||
|
LineupAvatar(
|
||||||
|
id=avatar_id,
|
||||||
|
slot=index,
|
||||||
|
hp=10000,
|
||||||
|
sp_bar=SpBarInfo(cur_sp=5000, max_sp=10000),
|
||||||
|
satiety=100,
|
||||||
|
avatar_type=AvatarType.AVATAR_FORMAL_TYPE.value
|
||||||
|
)
|
||||||
|
for index, avatar_id in enumerate(self.avatar_list)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def add_lineup(self):
|
||||||
|
lineup_data = self.model_dump()
|
||||||
|
lineups_collection.insert_one(lineup_data)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def save_lineup(self):
|
||||||
|
lineup_data = self.model_dump()
|
||||||
|
lineup_data["uid"] = self.uid
|
||||||
|
query = {"uid": self.uid, "index": self.index}
|
||||||
|
existing_lineup = lineups_collection.find_one(query)
|
||||||
|
|
||||||
|
if existing_lineup:
|
||||||
|
lineups_collection.update_one(query, {"$set": lineup_data})
|
||||||
|
else:
|
||||||
|
lineups_collection.insert_one(lineup_data)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_lineup_by_uid(uid: int) -> List[LineupData]:
|
||||||
|
lineups_data = lineups_collection.find({"uid": uid})
|
||||||
|
result = []
|
||||||
|
for lineup in lineups_data:
|
||||||
|
del lineup["_id"]
|
||||||
|
result.append(LineupData(**lineup))
|
||||||
|
return result
|
||||||
10
database/mongodb.py
Normal file
10
database/mongodb.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
client = MongoClient("mongodb://localhost:27017")
|
||||||
|
|
||||||
|
def get_database():
|
||||||
|
return client["neonsr"]
|
||||||
|
|
||||||
|
def get_collection(collection_name: str):
|
||||||
|
database = client["neonsr"]
|
||||||
|
return database[collection_name]
|
||||||
103
database/player/player_data.py
Normal file
103
database/player/player_data.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
from database.mongodb import get_collection
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from database.base_database_data import BaseDatabaseData
|
||||||
|
from rail_proto.lib import (
|
||||||
|
Gender,
|
||||||
|
PlayerBasicInfo,
|
||||||
|
PlayerDetailInfo,
|
||||||
|
PlatformType,
|
||||||
|
PlayerRecordInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
players_collection = get_collection("players")
|
||||||
|
|
||||||
|
class PositionModel(BaseModel):
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
z: int
|
||||||
|
|
||||||
|
class RotationModel(BaseModel):
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
z: int
|
||||||
|
|
||||||
|
class PlayerData(BaseDatabaseData):
|
||||||
|
name: str = ""
|
||||||
|
signature: str = ""
|
||||||
|
birthday: int = 0
|
||||||
|
cur_basic_type: int = 8001
|
||||||
|
head_icon: int = 208001
|
||||||
|
phone_theme: int = 221000
|
||||||
|
chat_bubble: int = 220000
|
||||||
|
current_bgm: int = 210007
|
||||||
|
current_gender: Gender = Gender.GenderMan
|
||||||
|
level: int = 70
|
||||||
|
exp: int = 0
|
||||||
|
world_level: int = 0
|
||||||
|
scoin: int = 0 # Credits
|
||||||
|
hcoin: int = 0 # Jade
|
||||||
|
mcoin: int = 0 # Crystals
|
||||||
|
plane_id: int = 10000
|
||||||
|
floor_id: int = 10000000
|
||||||
|
entry_id: int = 100000104
|
||||||
|
cur_lineup: int = 0
|
||||||
|
pos: Optional[PositionModel] = PositionModel(x=0,y=0,z=0)
|
||||||
|
rot: Optional[RotationModel] = RotationModel(x=0,y=0,z=0)
|
||||||
|
|
||||||
|
def ToProto(self) -> PlayerBasicInfo:
|
||||||
|
return PlayerBasicInfo(
|
||||||
|
nickname=self.name,
|
||||||
|
level=self.level,
|
||||||
|
exp=self.exp,
|
||||||
|
world_level=self.world_level,
|
||||||
|
scoin=self.scoin,
|
||||||
|
hcoin=self.hcoin,
|
||||||
|
mcoin=self.mcoin,
|
||||||
|
stamina=240
|
||||||
|
)
|
||||||
|
|
||||||
|
def ToDetailProto(self) -> PlayerDetailInfo:
|
||||||
|
return PlayerDetailInfo(
|
||||||
|
nickname=self.Name,
|
||||||
|
level=self.Level,
|
||||||
|
signature=self.Signature,
|
||||||
|
is_banned=False,
|
||||||
|
head_icon=self.HeadIcon,
|
||||||
|
platform=PlatformType.PC,
|
||||||
|
uid=self.uid,
|
||||||
|
world_level=self.WorldLevel,
|
||||||
|
record_info=PlayerRecordInfo()
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_player_data(self,name : str):
|
||||||
|
try:
|
||||||
|
self.name = name
|
||||||
|
player_data = self.model_dump()
|
||||||
|
players_collection.insert_one(player_data)
|
||||||
|
return PlayerData(**player_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating player data: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_player_data(self):
|
||||||
|
try:
|
||||||
|
player_data = self.model_dump()
|
||||||
|
player_data["uid"] = self.uid
|
||||||
|
query = {"uid": self.uid}
|
||||||
|
players_collection.update_one(query, {"$set": player_data})
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error menyimpan data pemain: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def find_player_by_uid(uid: int) -> Optional[PlayerData]:
|
||||||
|
player_data = players_collection.find_one({"uid": uid})
|
||||||
|
if player_data:
|
||||||
|
del player_data["_id"]
|
||||||
|
return PlayerData(**player_data)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
123
game_server/dummy.py
Normal file
123
game_server/dummy.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
dummyprotolist = [
|
||||||
|
"SceneEntityMove",
|
||||||
|
"GetMissionData",
|
||||||
|
"FinishTalkMission",
|
||||||
|
"InteractProp",
|
||||||
|
|
||||||
|
"GetFirstTalkNpc",
|
||||||
|
"GetAssistHistory",
|
||||||
|
"GetTrackPhotoActivityData",
|
||||||
|
"GetSwordTrainingData",
|
||||||
|
"GetSummonActivityData",
|
||||||
|
"GetMainMissionCustomValue"
|
||||||
|
"SetPlayerInfo",
|
||||||
|
"GetPlayerDetailInfo",
|
||||||
|
"GetLevelRewardTakenList",
|
||||||
|
"GetRogueScoreRewardInfo",
|
||||||
|
"GetGachaInfo",
|
||||||
|
"QueryProductInfo",
|
||||||
|
"GetQuestData",
|
||||||
|
"GetQuestRecord",
|
||||||
|
"GetFriendApplyListInfo",
|
||||||
|
"GetCurAssist",
|
||||||
|
"GetRogueHandbookData",
|
||||||
|
"GetDailyActiveInfo",
|
||||||
|
"GetFightActivityData",
|
||||||
|
"GetMultipleDropInfo",
|
||||||
|
"GetPlayerReturnMultiDropInfo",
|
||||||
|
"GetShareData",
|
||||||
|
"GetTreasureDungeonActivityData",
|
||||||
|
"PlayerReturnInfoQuery",
|
||||||
|
"GetActivityScheduleConfig",
|
||||||
|
|
||||||
|
|
||||||
|
"GetMissionEventData",
|
||||||
|
"GetRogueInfo",
|
||||||
|
"GetExpeditionData",
|
||||||
|
"GetRogueDialogueEventData",
|
||||||
|
"GetJukeboxData",
|
||||||
|
"SyncClientResVersion",
|
||||||
|
"DailyFirstMeetPam",
|
||||||
|
"GetMuseumInfo",
|
||||||
|
"GetLoginActivity",
|
||||||
|
"GetRaidInfo",
|
||||||
|
"GetTrialActivityData",
|
||||||
|
"GetBoxingClubInfo",
|
||||||
|
"GetNpcStatus",
|
||||||
|
"TextJoinQuery",
|
||||||
|
"GetSpringRecoverData",
|
||||||
|
"GetChatFriendHistory",
|
||||||
|
"GetSecretKeyInfo",
|
||||||
|
"GetVideoVersionKey",
|
||||||
|
"GetCurBattleInfo",
|
||||||
|
|
||||||
|
"GetMarkItemList",
|
||||||
|
"RogueTournGetCurRogueCocoonInfo",
|
||||||
|
"GetAllServerPrefsData",
|
||||||
|
"GetRogueCommonDialogueData",
|
||||||
|
"GetRogueEndlessActivityData",
|
||||||
|
"RogueArcadeGetInfo",
|
||||||
|
"ChessRogueQuery",
|
||||||
|
"RogueTournQuery",
|
||||||
|
"RogueMagicQuery",
|
||||||
|
"GetBattleCollegeData",
|
||||||
|
"GetHeartDialInfo",
|
||||||
|
"TrainPartyGetData",
|
||||||
|
"HeliobusActivityData",
|
||||||
|
"GetEnteredScene",
|
||||||
|
"GetAetherDivideInfo",
|
||||||
|
"GetMapRotationData",
|
||||||
|
"GetRogueCollection",
|
||||||
|
"GetRogueExhibition",
|
||||||
|
"GetPetData",
|
||||||
|
"EnterSection",
|
||||||
|
|
||||||
|
|
||||||
|
"GetMultiPathAvatarInfo",
|
||||||
|
"GetCurChallenge",
|
||||||
|
"GetMissionMessageInfo",
|
||||||
|
"SwitchHandData",
|
||||||
|
"GetPamSkinData",
|
||||||
|
"GetMainMissionCustomValue",
|
||||||
|
"GetNpcMessageGroup",
|
||||||
|
"GetRechargeGiftInfo",
|
||||||
|
"GetFriendLoginInfo",
|
||||||
|
"GetChessRogueNousStoryInfo",
|
||||||
|
"CommonRogueQuery",
|
||||||
|
"GetStarFightData",
|
||||||
|
"EvolveBuildQueryInfo",
|
||||||
|
"GetAlleyInfo",
|
||||||
|
"GetAetherDivideChallengeInfo",
|
||||||
|
"GetStrongChallengeActivityData",
|
||||||
|
"GetOfferingInfo",
|
||||||
|
"ClockParkGetInfo",
|
||||||
|
"MusicRhythmData",
|
||||||
|
"GetGunPlayData",
|
||||||
|
"GetFightFestData",
|
||||||
|
"DifficultyAdjustmentGetData",
|
||||||
|
"ChimeraGetData",
|
||||||
|
"MarbleGetData",
|
||||||
|
"GetRechargeBenefitInfo",
|
||||||
|
"ParkourGetData",
|
||||||
|
"SpaceZooData",
|
||||||
|
"GetMaterialSubmitActivityData",
|
||||||
|
"GetPreAvatarList",
|
||||||
|
"TravelBrochureGetData",
|
||||||
|
"RaidCollectionData",
|
||||||
|
"GetChatEmojiList",
|
||||||
|
"GetTelevisionActivityData",
|
||||||
|
"GetTrainVisitorRegister",
|
||||||
|
"GetLoginChatInfo",
|
||||||
|
"GetFeverTimeActivityData",
|
||||||
|
"TarotBookGetData",
|
||||||
|
"GetMarkChest",
|
||||||
|
"GetArchiveData",
|
||||||
|
"GetAllSaveRaid",
|
||||||
|
"MatchThreeGetData",
|
||||||
|
"GetDrinkMakerData",
|
||||||
|
"UpdateServerPrefsData",
|
||||||
|
"UpdateTrackMainMissionId",
|
||||||
|
"GetMail",
|
||||||
|
"GetShopList",
|
||||||
|
"PlayerLogout",
|
||||||
|
]
|
||||||
13
game_server/game/avatar/avatar_manager.py
Normal file
13
game_server/game/avatar/avatar_manager.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import dataclasses
|
||||||
|
from rail_proto.lib import AvatarSkillTree,EquipRelic
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AvatarManager:
|
||||||
|
avatar_id: int
|
||||||
|
level: int
|
||||||
|
exp: int
|
||||||
|
promotion: int
|
||||||
|
rank: int
|
||||||
|
skills: list[AvatarSkillTree]
|
||||||
|
equip_id: int = 0
|
||||||
|
relic_list: dict[int,EquipRelic] = dataclasses.field(default_factory=dict)
|
||||||
16
game_server/game/chat/command/__init__.py
Normal file
16
game_server/game/chat/command/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
folder = "game_server/game/chat/command"
|
||||||
|
sys.path.append(os.path.dirname(folder))
|
||||||
|
|
||||||
|
for filename in os.listdir(folder):
|
||||||
|
if filename.endswith(".py") and filename != "__init__.py":
|
||||||
|
module_name = filename[:-3]
|
||||||
|
module_path = f"game_server.game.chat.command.{module_name}"
|
||||||
|
try:
|
||||||
|
importlib.import_module(module_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error importing module '{module_path}': {e}")
|
||||||
60
game_server/game/chat/command/give.py
Normal file
60
game_server/game/chat/command/give.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from game_server.game.chat.decorators import Command
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.equipment_config import EquipmentConfig
|
||||||
|
from game_server.resource.configdb.relic_config import RelicConfigData
|
||||||
|
from game_server.resource.configdb.relic_main_affix_config import RelicMainAffixConfigData
|
||||||
|
from game_server.resource.configdb.relic_sub_affix_config import RelicSubAffixConfigData
|
||||||
|
from database.inventory.inventory_data import SubAffix
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
prefix="give",
|
||||||
|
usage="/give",
|
||||||
|
)
|
||||||
|
async def execute(session:PlayerSession, item_id, param1=None, param2=None, param3=None, param4=None, param5=None):
|
||||||
|
try:
|
||||||
|
sync = False
|
||||||
|
lightcones = ResourceManager.instance().find_by_index(EquipmentConfig,item_id)
|
||||||
|
relics = ResourceManager.instance().find_by_index(RelicConfigData,item_id)
|
||||||
|
|
||||||
|
if lightcones:
|
||||||
|
rank = 1
|
||||||
|
if param1 and param1.startswith("r") and len(param1) == 2:
|
||||||
|
rank_value = param1[1]
|
||||||
|
if rank_value.isdigit() and 1 <= int(rank_value) <= 5:
|
||||||
|
rank = int(rank_value)
|
||||||
|
elif rank_value.isdigit() and int(rank_value) > 5:
|
||||||
|
rank = 5
|
||||||
|
item = session.player.add_lightcone(item_id, rank)
|
||||||
|
if item:
|
||||||
|
session.player.inventory_manager[item.unique_id] = item
|
||||||
|
sync = True
|
||||||
|
elif relics:
|
||||||
|
main_stat = 0
|
||||||
|
main_property = ""
|
||||||
|
main_affix_group = ResourceManager.instance().find_all_by_index(RelicMainAffixConfigData, relics.MainAffixGroup)
|
||||||
|
sub_affix_list = []
|
||||||
|
|
||||||
|
if param1 and param1.startswith("s") and len(param1) == 2 and param1[1].isdigit():
|
||||||
|
main_affix = next((affix for affix in main_affix_group if affix.AffixID == int(param1[1])))
|
||||||
|
if main_affix:
|
||||||
|
main_stat = main_affix.AffixID
|
||||||
|
main_property = main_affix.Property
|
||||||
|
|
||||||
|
for param in [param2, param3, param4, param5]:
|
||||||
|
if param and len(param) >= 3 and ":" in param:
|
||||||
|
parts = param.split(":")
|
||||||
|
if len(parts) == 2 and all(part.isdigit() for part in parts):
|
||||||
|
sub_affix_data = ResourceManager.instance().find_all_by_index(RelicSubAffixConfigData, relics.SubAffixGroup)
|
||||||
|
for affix in sub_affix_data:
|
||||||
|
if affix.AffixID == int(parts[0]) and affix.Property != main_property:
|
||||||
|
sub_affix_list.append(SubAffix(id=int(parts[0]), count=int(parts[1]), step=int(parts[1]) * 2))
|
||||||
|
item = session.player.add_relic(item_id,main_stat,sub_affix_list)
|
||||||
|
if item:
|
||||||
|
session.player.inventory_manager[item.unique_id] = item
|
||||||
|
sync = True
|
||||||
|
if sync:
|
||||||
|
await session.notify(session.player.PlayerSyncProto())
|
||||||
|
return "GIVE"
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
48
game_server/game/chat/command/giveall.py
Normal file
48
game_server/game/chat/command/giveall.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from game_server.game.chat.decorators import Command
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.avatar_config import AvatarConfig
|
||||||
|
from game_server.resource.configdb.equipment_config import EquipmentConfig
|
||||||
|
from game_server.resource.configdb.relic_config import RelicConfigData
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
prefix="giveall",
|
||||||
|
usage="/giveall",
|
||||||
|
)
|
||||||
|
async def execute(session:PlayerSession, text):
|
||||||
|
try:
|
||||||
|
sync = False
|
||||||
|
if text == "avatars":
|
||||||
|
avatars = ResourceManager.instance().values(AvatarConfig)
|
||||||
|
for avatar in avatars:
|
||||||
|
if avatar.AvatarID == 1224 or avatar.AvatarID >= 7000:
|
||||||
|
continue
|
||||||
|
if session.player.avatar_mananger.get(avatar.AvatarID):
|
||||||
|
continue
|
||||||
|
data = session.player.add_avatar(avatar.AvatarID)
|
||||||
|
if data:
|
||||||
|
session.player.avatar_mananger[data.avatar_id] = data
|
||||||
|
sync = True
|
||||||
|
|
||||||
|
if text == "lightcones":
|
||||||
|
lightcones = ResourceManager.instance().values(EquipmentConfig)
|
||||||
|
for lightcone in lightcones:
|
||||||
|
item = session.player.add_lightcone(lightcone.EquipmentID)
|
||||||
|
if item:
|
||||||
|
session.player.inventory_manager[item.unique_id] = item
|
||||||
|
sync = True
|
||||||
|
|
||||||
|
if text == "relics":
|
||||||
|
relics = ResourceManager.instance().values(RelicConfigData)
|
||||||
|
for relic in relics:
|
||||||
|
item = session.player.add_relic(relic.ID)
|
||||||
|
if item:
|
||||||
|
session.player.inventory_manager[item.unique_id] = item
|
||||||
|
sync = True
|
||||||
|
|
||||||
|
if sync:
|
||||||
|
await session.notify(session.player.PlayerSyncProto())
|
||||||
|
|
||||||
|
return "GIVEALL"
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
53
game_server/game/chat/command_handler.py
Normal file
53
game_server/game/chat/command_handler.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from utils.logger import Info
|
||||||
|
from game_server.game.chat.decorators import command_registry
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
import game_server.game.chat.command # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHandler:
|
||||||
|
def load_commands(self):
|
||||||
|
registered_commands = ", ".join(
|
||||||
|
item.prefix for item in command_registry.values() if not item.is_alias
|
||||||
|
)
|
||||||
|
|
||||||
|
Info(
|
||||||
|
f"[BOOT] [CommandHandler] Registered {len(command_registry)} game commands => {registered_commands}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse_command(self, content: str):
|
||||||
|
content = content.lstrip("/")
|
||||||
|
parts = content.split(maxsplit=1)
|
||||||
|
if len(parts) < 2:
|
||||||
|
return parts[0], ""
|
||||||
|
|
||||||
|
return parts[0], parts[1]
|
||||||
|
|
||||||
|
def print_help(self):
|
||||||
|
result = "Available commands:\n"
|
||||||
|
for index, (_, func) in enumerate(command_registry.items()):
|
||||||
|
result += f"{index+1}) {func.usage}\n\n"
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def handle_command(self, session: PlayerSession, content: str):
|
||||||
|
if content == "/help":
|
||||||
|
return self.print_help()
|
||||||
|
|
||||||
|
command_label, args = self.parse_command(content)
|
||||||
|
command_func = command_registry.get(command_label)
|
||||||
|
|
||||||
|
if command_func is not None:
|
||||||
|
func_args_cnt = command_func.__code__.co_argcount - 1
|
||||||
|
args_list = args.split()[:func_args_cnt]
|
||||||
|
|
||||||
|
if args_list and args_list[0] == "help":
|
||||||
|
return f"Usage: {command_func.usage}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
return await command_func(session, *args_list)
|
||||||
|
except TypeError:
|
||||||
|
return f"Usage: {command_func.usage}"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
handler = CommandHandler()
|
||||||
21
game_server/game/chat/decorators.py
Normal file
21
game_server/game/chat/decorators.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from typing import Dict, Type
|
||||||
|
|
||||||
|
|
||||||
|
command_registry: Dict[str, Type] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def Command(prefix: str, usage: str, aliases: list = list()):
|
||||||
|
def decorator(func):
|
||||||
|
func.usage = usage
|
||||||
|
func.prefix = prefix
|
||||||
|
func.is_alias = False
|
||||||
|
command_registry[prefix] = func
|
||||||
|
|
||||||
|
# Register alias if exist
|
||||||
|
for alias in aliases:
|
||||||
|
func.is_alias = True
|
||||||
|
command_registry[alias] = func
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
10
game_server/game/enums/avatar/relic_type.py
Normal file
10
game_server/game/enums/avatar/relic_type.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class RelicTypeEnum(Enum):
|
||||||
|
Unknown = 0
|
||||||
|
HEAD = 1
|
||||||
|
HAND = 2
|
||||||
|
BODY = 3
|
||||||
|
FOOT = 4
|
||||||
|
NECK = 5
|
||||||
|
OBJECT = 6
|
||||||
12
game_server/game/enums/scene/game_mode_type.py
Normal file
12
game_server/game/enums/scene/game_mode_type.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class GameModeTypeEnum(Enum):
|
||||||
|
Unknown = 0
|
||||||
|
Town = 1
|
||||||
|
Maze = 2
|
||||||
|
Train = 3
|
||||||
|
Challenge = 4
|
||||||
|
Rogue = 5
|
||||||
|
Raid = 6
|
||||||
|
AetherDivide = 7
|
||||||
|
TrialActivity = 8
|
||||||
57
game_server/game/items/relic_manager.py
Normal file
57
game_server/game/items/relic_manager.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import random
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.relic_config import RelicConfigData
|
||||||
|
from game_server.resource.configdb.relic_main_affix_config import RelicMainAffixConfigData
|
||||||
|
from game_server.resource.configdb.relic_sub_affix_config import RelicSubAffixConfigData
|
||||||
|
|
||||||
|
|
||||||
|
class SubAffix(BaseModel):
|
||||||
|
id: int
|
||||||
|
count: int
|
||||||
|
step: int
|
||||||
|
|
||||||
|
class MainAffix(BaseModel):
|
||||||
|
AffixID: int
|
||||||
|
Property: str
|
||||||
|
|
||||||
|
class RelicManager(BaseModel):
|
||||||
|
def GetRandomRelicMainAffix(self, itemid) -> MainAffix:
|
||||||
|
config = ResourceManager.instance().find_by_index(RelicConfigData, itemid)
|
||||||
|
if not config:
|
||||||
|
return MainAffix(AffixID=0, Property=None)
|
||||||
|
|
||||||
|
affixes = [
|
||||||
|
affix
|
||||||
|
for affix in ResourceManager.instance().values(RelicMainAffixConfigData)
|
||||||
|
if config.MainAffixGroup == affix.GroupID
|
||||||
|
]
|
||||||
|
|
||||||
|
if not affixes:
|
||||||
|
return MainAffix(AffixID=0, Property=None)
|
||||||
|
|
||||||
|
selected_affix = random.choice(affixes)
|
||||||
|
return MainAffix(AffixID=selected_affix.AffixID, Property=selected_affix.Property)
|
||||||
|
|
||||||
|
def GetRandomRelicSubAffix(self, itemid: int, property: str, loop: int, affix_list: list[SubAffix] = []) -> list[SubAffix]:
|
||||||
|
config = ResourceManager.instance().find_by_index(RelicConfigData, itemid)
|
||||||
|
if not config:
|
||||||
|
return []
|
||||||
|
|
||||||
|
affix_ids_in_list = {affix.id for affix in affix_list}
|
||||||
|
affixes = [
|
||||||
|
affix
|
||||||
|
for affix in ResourceManager.instance().values(RelicSubAffixConfigData)
|
||||||
|
if config.SubAffixGroup == affix.GroupID and affix.Property != property and affix.AffixID not in affix_ids_in_list
|
||||||
|
]
|
||||||
|
|
||||||
|
selected_affixes = random.sample(affixes, 4 - len(affix_ids_in_list))
|
||||||
|
|
||||||
|
sub_affix_list = []
|
||||||
|
for affix in selected_affixes:
|
||||||
|
count = random.randint(1, loop)
|
||||||
|
step = count * 2
|
||||||
|
sub_affix_list.append(SubAffix(id=affix.AffixID, count=count, step=step))
|
||||||
|
|
||||||
|
return sub_affix_list
|
||||||
|
|
||||||
22
game_server/game/motion/motion_info.py
Normal file
22
game_server/game/motion/motion_info.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from rail_proto.lib import MotionInfo,Vector
|
||||||
|
|
||||||
|
class Motion(BaseModel):
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
z: float
|
||||||
|
rotY: float
|
||||||
|
|
||||||
|
def ToProto(self) -> MotionInfo:
|
||||||
|
return MotionInfo(
|
||||||
|
pos=Vector(
|
||||||
|
x=int(self.x*1000),
|
||||||
|
y=int(self.y*1000),
|
||||||
|
z=int(self.z*1000)
|
||||||
|
),
|
||||||
|
rot=Vector(
|
||||||
|
x=0,
|
||||||
|
y=int(self.rotY*1000),
|
||||||
|
z=0
|
||||||
|
)
|
||||||
|
)
|
||||||
171
game_server/game/player/player_manager.py
Normal file
171
game_server/game/player/player_manager.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from database.player.player_data import PlayerData,players_collection
|
||||||
|
from database.avatar.avatar_data import AvatarData,get_all_avatars_by_uid,avatars_collection
|
||||||
|
from database.lineup.lineup_data import LineupData,get_all_lineup_by_uid,lineups_collection
|
||||||
|
from database.inventory.inventory_data import InventoryData,get_all_items_by_uid,items_collection
|
||||||
|
from game_server.game.scene.scene_manager import SceneManager
|
||||||
|
from typing import Optional
|
||||||
|
from rail_proto.lib import (
|
||||||
|
PlayerSyncScNotify,
|
||||||
|
AvatarSync
|
||||||
|
)
|
||||||
|
from pymongo import UpdateOne
|
||||||
|
from utils.logger import Error
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerManager(BaseModel):
|
||||||
|
data : PlayerData = PlayerData()
|
||||||
|
avatar_mananger: dict[int,AvatarData] = {}
|
||||||
|
lineup_manager: dict[int,LineupData] = {}
|
||||||
|
inventory_manager: dict[int,InventoryData] = {}
|
||||||
|
scene_manager: SceneManager = None
|
||||||
|
|
||||||
|
def init_default(self):
|
||||||
|
self.add_all_avatars()
|
||||||
|
self.add_all_lineups()
|
||||||
|
self.add_all_items()
|
||||||
|
self.scene_manager = SceneManager(
|
||||||
|
entry_id=self.data.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_all(self):
|
||||||
|
self.save_player_data_bulk()
|
||||||
|
self.save_all_avatars_bulk()
|
||||||
|
self.save_all_lineups_bulk()
|
||||||
|
self.save_all_items_bulk()
|
||||||
|
|
||||||
|
def add_all_avatars(self):
|
||||||
|
avatars = get_all_avatars_by_uid(uid=self.data.uid)
|
||||||
|
if not avatars:
|
||||||
|
avatar = AvatarData(
|
||||||
|
uid=self.data.uid,
|
||||||
|
avatar_id=self.data.cur_basic_type
|
||||||
|
).add_avatar()
|
||||||
|
self.avatar_mananger[self.data.cur_basic_type] = avatar
|
||||||
|
|
||||||
|
for avatar in avatars:
|
||||||
|
self.avatar_mananger[avatar.avatar_id] = avatar
|
||||||
|
|
||||||
|
def add_all_lineups(self):
|
||||||
|
lineups = get_all_lineup_by_uid(uid=self.data.uid)
|
||||||
|
if not lineups:
|
||||||
|
lineup = LineupData(
|
||||||
|
uid=self.data.uid,
|
||||||
|
index=0,
|
||||||
|
name="",
|
||||||
|
avatar_list=[self.data.cur_basic_type]
|
||||||
|
).add_lineup()
|
||||||
|
self.lineup_manager[lineup.index] = lineup
|
||||||
|
for lineup in lineups:
|
||||||
|
self.lineup_manager[lineup.index] = lineup
|
||||||
|
|
||||||
|
def add_all_items(self):
|
||||||
|
items = get_all_items_by_uid(uid=self.data.uid)
|
||||||
|
for item in items:
|
||||||
|
self.inventory_manager[item.unique_id] = item
|
||||||
|
|
||||||
|
def add_avatar(self,avatar_id:int) -> Optional[AvatarData]:
|
||||||
|
avatar = self.avatar_mananger.get(avatar_id)
|
||||||
|
if avatar:
|
||||||
|
return None
|
||||||
|
|
||||||
|
avatar = AvatarData(uid=self.data.uid,avatar_id=avatar_id).add_avatar()
|
||||||
|
if avatar:
|
||||||
|
return avatar
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_lineup(self,avatar_ids: list[int],index=0,name=""):
|
||||||
|
LineupData(
|
||||||
|
uid=self.data.uid,
|
||||||
|
index=index,
|
||||||
|
name=name,
|
||||||
|
avatar_list=avatar_ids
|
||||||
|
).add_lineup()
|
||||||
|
|
||||||
|
def add_lightcone(self,lightcone_id: int, rank=1) -> Optional[InventoryData]:
|
||||||
|
item = InventoryData(
|
||||||
|
uid=self.data.uid,
|
||||||
|
item_id=lightcone_id,
|
||||||
|
rank=rank
|
||||||
|
).add_lightcone()
|
||||||
|
if item:
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_relic(self,relic_id: int, main_affix=0, sub_affix=[]) -> Optional[InventoryData]:
|
||||||
|
item = InventoryData(
|
||||||
|
uid=self.data.uid,
|
||||||
|
item_id=relic_id,
|
||||||
|
main_affix=main_affix,
|
||||||
|
sub_affix=sub_affix
|
||||||
|
).add_relic()
|
||||||
|
if item:
|
||||||
|
return item
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def PlayerSyncProto(self) -> PlayerSyncScNotify:
|
||||||
|
avatars = []
|
||||||
|
for avatar_id,avatar in self.avatar_mananger.items():
|
||||||
|
avatars.append(avatar.ToProto())
|
||||||
|
|
||||||
|
lightcones = []
|
||||||
|
for unique_id,item in self.inventory_manager.items():
|
||||||
|
if item.main_type == 1:
|
||||||
|
lightcones.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=lightcones,
|
||||||
|
relic_list=relics,
|
||||||
|
avatar_sync=
|
||||||
|
AvatarSync(
|
||||||
|
avatar_list=avatars
|
||||||
|
),
|
||||||
|
basic_info=self.data.ToProto()
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_player_data_bulk(self):
|
||||||
|
try:
|
||||||
|
player_data = self.data.model_dump()
|
||||||
|
player_data["uid"] = self.data.uid
|
||||||
|
query = {"uid": self.data.uid}
|
||||||
|
players_collection.update_one(query, {"$set": player_data}, upsert=True)
|
||||||
|
except Exception as e:
|
||||||
|
Error(f"Error save player data: {e}")
|
||||||
|
|
||||||
|
def save_all_avatars_bulk(self):
|
||||||
|
operations = []
|
||||||
|
for avatar in self.avatar_mananger.values():
|
||||||
|
avatar_data = avatar.model_dump()
|
||||||
|
avatar_data["uid"] = avatar.uid
|
||||||
|
query = {"uid": avatar.uid, "avatar_id": avatar.avatar_id}
|
||||||
|
operations.append(UpdateOne(query, {"$set": avatar_data}, upsert=True))
|
||||||
|
|
||||||
|
if operations:
|
||||||
|
avatars_collection.bulk_write(operations)
|
||||||
|
|
||||||
|
def save_all_lineups_bulk(self):
|
||||||
|
operations = []
|
||||||
|
for lineup in self.lineup_manager.values():
|
||||||
|
lineup_data = lineup.model_dump()
|
||||||
|
lineup_data["uid"] = lineup.uid
|
||||||
|
query = {"uid": lineup.uid, "index": lineup.index}
|
||||||
|
operations.append(UpdateOne(query, {"$set": lineup_data}, upsert=True))
|
||||||
|
|
||||||
|
if operations:
|
||||||
|
lineups_collection.bulk_write(operations)
|
||||||
|
|
||||||
|
def save_all_items_bulk(self):
|
||||||
|
operations = []
|
||||||
|
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))
|
||||||
|
|
||||||
|
if operations:
|
||||||
|
items_collection.bulk_write(operations)
|
||||||
165
game_server/game/scene/scene_manager.py
Normal file
165
game_server/game/scene/scene_manager.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from game_server.game.enums.scene.game_mode_type import GameModeTypeEnum
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.maze_plane import MazePlaneData
|
||||||
|
from game_server.resource.configdb.map_entrance import MapEntranceData
|
||||||
|
from game_server.game.motion.motion_info import Motion
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SceneEntityGroupInfo,
|
||||||
|
SceneInfo,
|
||||||
|
SceneEntityInfo,
|
||||||
|
SceneEntityGroupInfo,
|
||||||
|
ScenePropInfo,
|
||||||
|
SceneNpcInfo,
|
||||||
|
SceneNpcMonsterInfo,
|
||||||
|
SceneActorInfo,
|
||||||
|
AvatarType
|
||||||
|
)
|
||||||
|
|
||||||
|
class SceneManager(BaseModel):
|
||||||
|
entry_id: int
|
||||||
|
plane_id: int = 0
|
||||||
|
floor_id: int = 0
|
||||||
|
game_mode_type: int = 0
|
||||||
|
world_id: int = 0
|
||||||
|
teleport_id: int = 0
|
||||||
|
entity_group_list: list[SceneEntityGroupInfo] = []
|
||||||
|
entities: dict[int,SceneEntityInfo] = {}
|
||||||
|
battle_monster: list[SceneEntityInfo] = []
|
||||||
|
|
||||||
|
def ToProto(self,session) -> SceneInfo:
|
||||||
|
prop_entity_id = 10
|
||||||
|
npc_entity_id = 10_000
|
||||||
|
monster_entity_id = 20_000
|
||||||
|
entrance = ResourceManager.instance().find_by_index(MapEntranceData, self.entry_id)
|
||||||
|
maze_plane = ResourceManager.instance().find_by_index(MazePlaneData, entrance.PlaneID)
|
||||||
|
floor_infos = entrance.floor_infos.get(self.entry_id)
|
||||||
|
proto = SceneInfo(
|
||||||
|
entry_id=self.entry_id,
|
||||||
|
plane_id=entrance.PlaneID,
|
||||||
|
floor_id=entrance.FloorID,
|
||||||
|
)
|
||||||
|
|
||||||
|
proto.game_mode_type = GameModeTypeEnum[maze_plane.PlaneType].value
|
||||||
|
if maze_plane.WorldID == 100:
|
||||||
|
maze_plane.WorldID = 401
|
||||||
|
proto.world_id = maze_plane.WorldID
|
||||||
|
|
||||||
|
loaded_npc = [13199,13321,1313,13227,13190,13191,13192,13141,13246,13407 ,13223,13152]
|
||||||
|
loaded_group = []
|
||||||
|
groups = []
|
||||||
|
for group_id,group in floor_infos.groups.items():
|
||||||
|
if group.LoadSide == "Client":
|
||||||
|
continue
|
||||||
|
# if group_id in loaded_group:
|
||||||
|
# continue
|
||||||
|
# if group_id > 150 and group_id < 200:
|
||||||
|
# continue
|
||||||
|
# if group_id > 300:
|
||||||
|
# continue
|
||||||
|
if group_id in groups:
|
||||||
|
continue
|
||||||
|
groups.append(group_id)
|
||||||
|
group_info = SceneEntityGroupInfo(
|
||||||
|
group_id=group_id
|
||||||
|
)
|
||||||
|
|
||||||
|
for prop in group.PropList:
|
||||||
|
prop_entity_id += 1
|
||||||
|
prop_entity_info = SceneEntityInfo(
|
||||||
|
inst_id=prop.ID,
|
||||||
|
group_id=group_id,
|
||||||
|
motion=Motion(
|
||||||
|
x=prop.PosX,
|
||||||
|
y=prop.PosY,
|
||||||
|
z=prop.PosZ,
|
||||||
|
rotY=prop.RotY
|
||||||
|
).ToProto(),
|
||||||
|
prop=ScenePropInfo(
|
||||||
|
prop_id=prop.PropID,
|
||||||
|
prop_state=8 if prop.MappingInfoID > 0 else (1 if prop.State == 0 else prop.State)
|
||||||
|
),
|
||||||
|
entity_id=prop_entity_id
|
||||||
|
)
|
||||||
|
group_info.entity_list.append(prop_entity_info)
|
||||||
|
if self.teleport_id > 0 and self.teleport_id == prop.MappingInfoID:
|
||||||
|
for anchor in group.AnchorList:
|
||||||
|
if group.id == prop.AnchorGroupID and anchor.ID == prop.AnchorID:
|
||||||
|
session.player.data.pos.x = int(anchor.PosX)
|
||||||
|
session.player.data.pos.y = int(anchor.PosY)
|
||||||
|
session.player.data.pos.z = int(anchor.PosZ)
|
||||||
|
session.player.data.rot.y = int(anchor.RotY)
|
||||||
|
|
||||||
|
for npc in group.NPCList:
|
||||||
|
if npc.NPCID in loaded_npc:
|
||||||
|
continue
|
||||||
|
loaded_npc.append(npc.NPCID)
|
||||||
|
npc_entity_id += 1
|
||||||
|
npc_entity_info = SceneEntityInfo(
|
||||||
|
inst_id=npc.ID,
|
||||||
|
group_id=group_id,
|
||||||
|
entity_id=npc_entity_id,
|
||||||
|
motion=Motion(
|
||||||
|
x=npc.PosX,
|
||||||
|
y=npc.PosY,
|
||||||
|
z=npc.PosZ,
|
||||||
|
rotY=npc.RotY
|
||||||
|
).ToProto(),
|
||||||
|
npc=SceneNpcInfo(
|
||||||
|
npc_id=npc.NPCID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
group_info.entity_list.append(npc_entity_info)
|
||||||
|
|
||||||
|
for monster in group.MonsterList:
|
||||||
|
monster_entity_id += 1
|
||||||
|
monster_entity_info = SceneEntityInfo(
|
||||||
|
inst_id=monster.ID,
|
||||||
|
group_id=group_id,
|
||||||
|
entity_id=monster_entity_id,
|
||||||
|
motion=Motion(
|
||||||
|
x=monster.PosX,
|
||||||
|
y=monster.PosY,
|
||||||
|
z=monster.PosZ,
|
||||||
|
rotY=monster.RotY
|
||||||
|
).ToProto(),
|
||||||
|
npc_monster=SceneNpcMonsterInfo(
|
||||||
|
monster_id=monster.NPCMonsterID,
|
||||||
|
event_id=monster.EventID,
|
||||||
|
world_level=6
|
||||||
|
)
|
||||||
|
)
|
||||||
|
group_info.entity_list.append(monster_entity_info)
|
||||||
|
session.player.scene_manager.entities[monster_entity_id] = monster_entity_info
|
||||||
|
proto.entity_group_list.append(group_info)
|
||||||
|
|
||||||
|
player_pos = Motion(
|
||||||
|
x=session.player.data.pos.x,
|
||||||
|
y=session.player.data.pos.y,
|
||||||
|
z=session.player.data.pos.z,
|
||||||
|
rotY=session.player.data.rot.y
|
||||||
|
).ToProto()
|
||||||
|
player_group = SceneEntityGroupInfo(state=0,group_id=0)
|
||||||
|
for avatar_id in session.player.lineup_manager.get(session.player.data.cur_lineup).avatar_list:
|
||||||
|
player_entity = SceneEntityInfo(
|
||||||
|
inst_id=0,
|
||||||
|
entity_id=avatar_id << 20,
|
||||||
|
motion=player_pos,
|
||||||
|
actor=SceneActorInfo(
|
||||||
|
avatar_type=AvatarType.AVATAR_FORMAL_TYPE.value,
|
||||||
|
base_avatar_id=avatar_id,
|
||||||
|
uid=session.player.data.uid,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
player_group.entity_list.append(player_entity)
|
||||||
|
proto.entity_group_list.append(player_group)
|
||||||
|
session.player.data.entry_id = self.entry_id
|
||||||
|
session.player.data.plane_id = entrance.PlaneID
|
||||||
|
session.player.data.floor_id = entrance.FloorID
|
||||||
|
return proto
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
13
game_server/handlers/ChangeLineupLeaderCsReq.py
Normal file
13
game_server/handlers/ChangeLineupLeaderCsReq.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
ChangeLineupLeaderCsReq,
|
||||||
|
ChangeLineupLeaderScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: ChangeLineupLeaderCsReq) -> betterproto.Message:
|
||||||
|
session.player.lineup_manager.get(session.player.data.cur_lineup,None).leader_slot = msg.slot
|
||||||
|
return ChangeLineupLeaderScRsp(
|
||||||
|
retcode=0,
|
||||||
|
slot=msg.slot
|
||||||
|
)
|
||||||
38
game_server/handlers/DressAvatarCsReq.py
Normal file
38
game_server/handlers/DressAvatarCsReq.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
DressAvatarCsReq,
|
||||||
|
DressAvatarScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: DressAvatarCsReq) -> betterproto.Message:
|
||||||
|
equipment = session.player.inventory_manager.get(msg.equipment_unique_id)
|
||||||
|
target_avatar = session.player.avatar_mananger.get(msg.avatar_id)
|
||||||
|
|
||||||
|
if not equipment or not target_avatar:
|
||||||
|
return DressAvatarScRsp()
|
||||||
|
|
||||||
|
previous_avatar = session.player.avatar_mananger.get(equipment.equip_avatar)
|
||||||
|
if previous_avatar:
|
||||||
|
if previous_avatar.lightcone_id > 0 and target_avatar.lightcone_id > 0:
|
||||||
|
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)
|
||||||
73
game_server/handlers/DressRelicAvatarCsReq.py
Normal file
73
game_server/handlers/DressRelicAvatarCsReq.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
DressRelicAvatarCsReq,
|
||||||
|
DressRelicAvatarScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: DressRelicAvatarCsReq) -> betterproto.Message:
|
||||||
|
target_avatar = session.player.avatar_mananger.get(msg.avatar_id)
|
||||||
|
if not target_avatar:
|
||||||
|
return DressRelicAvatarScRsp()
|
||||||
|
|
||||||
|
for relic_data in msg.switch_list:
|
||||||
|
relic = session.player.inventory_manager.get(relic_data.relic_unique_id)
|
||||||
|
if not relic:
|
||||||
|
continue
|
||||||
|
|
||||||
|
relic_sub_type = str(relic.sub_type)
|
||||||
|
current_relic_id = target_avatar.relic_ids.get(relic_sub_type, 0)
|
||||||
|
previous_avatar = session.player.avatar_mananger.get(relic.equip_avatar)
|
||||||
|
|
||||||
|
if previous_avatar:
|
||||||
|
previous_relic_id = previous_avatar.relic_ids.get(relic_sub_type, 0)
|
||||||
|
|
||||||
|
if previous_relic_id > 0 and current_relic_id > 0:
|
||||||
|
current_relic = session.player.inventory_manager.get(current_relic_id)
|
||||||
|
previous_relic = session.player.inventory_manager.get(previous_relic_id)
|
||||||
|
|
||||||
|
if current_relic and previous_relic:
|
||||||
|
previous_avatar.relic_ids[relic_sub_type] = current_relic.unique_id
|
||||||
|
current_relic.equip_avatar = previous_avatar.avatar_id
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
return DressRelicAvatarScRsp(retcode=0)
|
||||||
39
game_server/handlers/EnterSceneCsReq.py
Normal file
39
game_server/handlers/EnterSceneCsReq.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.game.motion.motion_info import Motion
|
||||||
|
from rail_proto.lib import (
|
||||||
|
EnterSceneCsReq,
|
||||||
|
EnterSceneScRsp,
|
||||||
|
EnterSceneByServerScNotify,
|
||||||
|
SceneEntityMoveScNotify
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: EnterSceneCsReq) -> betterproto.Message:
|
||||||
|
scene = session.player.scene_manager
|
||||||
|
scene.entry_id = msg.entry_id
|
||||||
|
if msg.teleport_id > 0:
|
||||||
|
session.player.scene_manager.teleport_id = msg.teleport_id
|
||||||
|
scene_proto = scene.ToProto(session)
|
||||||
|
lineup = session.player.lineup_manager.get(session.player.data.cur_lineup).ToProto()
|
||||||
|
session.pending_notify(
|
||||||
|
EnterSceneByServerScNotify(
|
||||||
|
scene=scene_proto,
|
||||||
|
lineup=lineup
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session.pending_notify(
|
||||||
|
SceneEntityMoveScNotify(
|
||||||
|
entry_id=session.player.scene_manager.entry_id,
|
||||||
|
motion=Motion(
|
||||||
|
x=session.player.data.pos.x,
|
||||||
|
y=session.player.data.pos.y,
|
||||||
|
z=session.player.data.pos.z,
|
||||||
|
rotY=session.player.data.rot.y,
|
||||||
|
).ToProto()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
scene.teleport_id = 0
|
||||||
|
#session.player.data.save_player_data()
|
||||||
|
return EnterSceneScRsp(
|
||||||
|
retcode=0
|
||||||
|
)
|
||||||
16
game_server/handlers/GetAllLineupDataCsReq.py
Normal file
16
game_server/handlers/GetAllLineupDataCsReq.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetAllLineupDataCsReq,
|
||||||
|
GetAllLineupDataScRsp,
|
||||||
|
LineupInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetAllLineupDataCsReq) -> betterproto.Message:
|
||||||
|
lineups: list[LineupInfo] = []
|
||||||
|
for index,lineup in session.player.lineup_manager.items():
|
||||||
|
lineups.append(lineup.ToProto())
|
||||||
|
return GetAllLineupDataScRsp(
|
||||||
|
retcode=0,
|
||||||
|
lineup_list=lineups
|
||||||
|
)
|
||||||
16
game_server/handlers/GetAvatarDataCsReq.py
Normal file
16
game_server/handlers/GetAvatarDataCsReq.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetAvatarDataCsReq,
|
||||||
|
GetAvatarDataScRsp,
|
||||||
|
Avatar
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetAvatarDataCsReq) -> betterproto.Message:
|
||||||
|
avatars: list[Avatar] = []
|
||||||
|
for avatar_id,avatar in session.player.avatar_mananger.items():
|
||||||
|
avatars.append(avatar.ToProto())
|
||||||
|
return GetAvatarDataScRsp(
|
||||||
|
retcode=0,
|
||||||
|
avatar_list=avatars
|
||||||
|
)
|
||||||
15
game_server/handlers/GetBagCsReq.py
Normal file
15
game_server/handlers/GetBagCsReq.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetBagCsReq,
|
||||||
|
GetBagScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetBagCsReq) -> betterproto.Message:
|
||||||
|
equipment = [equipment.ToProto() for unique_id,equipment in session.player.inventory_manager.items() if equipment.main_type == 1]
|
||||||
|
relics = [relic.ToProto() for unique_id,relic in session.player.inventory_manager.items() if relic.main_type == 2]
|
||||||
|
return GetBagScRsp(
|
||||||
|
retcode=0,
|
||||||
|
equipment_list=equipment,
|
||||||
|
relic_list=relics
|
||||||
|
)
|
||||||
24
game_server/handlers/GetBasicInfoCsReq.py
Normal file
24
game_server/handlers/GetBasicInfoCsReq.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetBasicInfoCsReq,
|
||||||
|
GetBasicInfoScRsp,
|
||||||
|
PlayerSettingInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetBasicInfoCsReq) -> betterproto.Message:
|
||||||
|
return GetBasicInfoScRsp(
|
||||||
|
retcode=0,
|
||||||
|
player_setting_info=PlayerSettingInfo(
|
||||||
|
mmmnjchemfn=True,
|
||||||
|
kapdimgjlnf=True,
|
||||||
|
ilfalcdlaol=True,
|
||||||
|
gmjanojmkce=True,
|
||||||
|
pbkbglhhkpe=True,
|
||||||
|
nkekibnjmpa=True,
|
||||||
|
kjncckhjfhe=True,
|
||||||
|
aicnfaobcpi=True,
|
||||||
|
aponeidmphl=True,
|
||||||
|
njfmiljofok=True
|
||||||
|
)
|
||||||
|
)
|
||||||
20
game_server/handlers/GetChallengeCsReq.py
Normal file
20
game_server/handlers/GetChallengeCsReq.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.challenge_maze_config import ChallengeMazeConfig
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetChallengeCsReq,
|
||||||
|
GetChallengeScRsp,
|
||||||
|
Challenge
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetChallengeCsReq) -> betterproto.Message:
|
||||||
|
return GetChallengeScRsp(
|
||||||
|
retcode=0,
|
||||||
|
challenge_list=[
|
||||||
|
Challenge(
|
||||||
|
challenge_id=challenge.ID
|
||||||
|
)
|
||||||
|
for challenge in ResourceManager.instance().values(ChallengeMazeConfig)
|
||||||
|
]
|
||||||
|
)
|
||||||
12
game_server/handlers/GetCurLineupDataCsReq.py
Normal file
12
game_server/handlers/GetCurLineupDataCsReq.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetCurLineupDataCsReq,
|
||||||
|
GetCurLineupDataScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetCurLineupDataCsReq) -> betterproto.Message:
|
||||||
|
return GetCurLineupDataScRsp(
|
||||||
|
retcode=0,
|
||||||
|
lineup=session.player.lineup_manager.get(session.player.data.cur_lineup,None).ToProto()
|
||||||
|
)
|
||||||
25
game_server/handlers/GetCurSceneInfoCsReq.py
Normal file
25
game_server/handlers/GetCurSceneInfoCsReq.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.game.motion.motion_info import Motion
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetCurSceneInfoCsReq,
|
||||||
|
GetCurSceneInfoScRsp,
|
||||||
|
SceneEntityMoveScNotify
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetCurSceneInfoCsReq) -> betterproto.Message:
|
||||||
|
session.pending_notify(
|
||||||
|
SceneEntityMoveScNotify(
|
||||||
|
entry_id=session.player.scene_manager.entry_id,
|
||||||
|
motion=Motion(
|
||||||
|
x=session.player.data.pos.x,
|
||||||
|
y=session.player.data.pos.y,
|
||||||
|
z=session.player.data.pos.z,
|
||||||
|
rotY=session.player.data.rot.y,
|
||||||
|
).ToProto()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return GetCurSceneInfoScRsp(
|
||||||
|
retcode=0,
|
||||||
|
scene=session.player.scene_manager.ToProto(session=session)
|
||||||
|
)
|
||||||
29
game_server/handlers/GetFriendListInfoCsReq.py
Normal file
29
game_server/handlers/GetFriendListInfoCsReq.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetFriendListInfoCsReq,
|
||||||
|
GetFriendListInfoScRsp,
|
||||||
|
FriendSimpleInfo,
|
||||||
|
PlayerSimpleInfo,
|
||||||
|
PlatformType,
|
||||||
|
FriendOnlineStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetFriendListInfoCsReq) -> betterproto.Message:
|
||||||
|
return GetFriendListInfoScRsp(
|
||||||
|
retcode=0,
|
||||||
|
friend_list=[
|
||||||
|
FriendSimpleInfo(
|
||||||
|
player_info=PlayerSimpleInfo(
|
||||||
|
uid=69,
|
||||||
|
level=1,
|
||||||
|
chat_bubble_id=220004,
|
||||||
|
head_icon=201310,
|
||||||
|
platform=PlatformType.PC.value,
|
||||||
|
online_status=FriendOnlineStatus.FRIEND_ONLINE_STATUS_ONLINE.value,
|
||||||
|
is_banned=False,
|
||||||
|
nickname="FireFly"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
26
game_server/handlers/GetMissionStatusCsReq.py
Normal file
26
game_server/handlers/GetMissionStatusCsReq.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.main_mission import MainMissionData
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetMissionStatusCsReq,
|
||||||
|
GetMissionStatusScRsp,
|
||||||
|
Mission,
|
||||||
|
MissionStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetMissionStatusCsReq) -> betterproto.Message:
|
||||||
|
return GetMissionStatusScRsp(
|
||||||
|
retcode=0,
|
||||||
|
finished_main_mission_id_list=[mission.MainMissionID for mission in ResourceManager.instance().values(MainMissionData)],
|
||||||
|
sub_mission_status_list=[
|
||||||
|
Mission(
|
||||||
|
id=mission_id,
|
||||||
|
progress=1,
|
||||||
|
status=MissionStatus.MISSION_FINISH.value
|
||||||
|
)
|
||||||
|
for mission_id in msg.sub_mission_id_list
|
||||||
|
],
|
||||||
|
unfinished_main_mission_id_list=[],
|
||||||
|
disabled_main_mission_id_list=[]
|
||||||
|
)
|
||||||
13
game_server/handlers/GetNpcTakenRewardCsReq.py
Normal file
13
game_server/handlers/GetNpcTakenRewardCsReq.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetNpcTakenRewardCsReq,
|
||||||
|
GetNpcTakenRewardScRsp,
|
||||||
|
PlayerSettingInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetNpcTakenRewardCsReq) -> betterproto.Message:
|
||||||
|
return GetNpcTakenRewardScRsp(
|
||||||
|
retcode=0,
|
||||||
|
npc_id=msg.npc_id
|
||||||
|
)
|
||||||
17
game_server/handlers/GetPhoneDataCsReq.py
Normal file
17
game_server/handlers/GetPhoneDataCsReq.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.item_config import ItemConfig
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetPhoneDataCsReq,
|
||||||
|
GetPhoneDataScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetPhoneDataCsReq) -> betterproto.Message:
|
||||||
|
return GetPhoneDataScRsp(
|
||||||
|
retcode=0,
|
||||||
|
cur_chat_bubble=session.player.data.chat_bubble,
|
||||||
|
cur_phone_theme=session.player.data.phone_theme,
|
||||||
|
owned_chat_bubbles=[item.ID for item in ResourceManager.instance().values(ItemConfig) if item.ItemSubType == "ChatBubble"],
|
||||||
|
owned_phone_themes=[item.ID for item in ResourceManager.instance().values(ItemConfig) if item.ItemSubType == "PhoneTheme"]
|
||||||
|
)
|
||||||
21
game_server/handlers/GetPlayerBoardDataCsReq.py
Normal file
21
game_server/handlers/GetPlayerBoardDataCsReq.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.player_icon_config import PlayerIconConfig
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetPlayerBoardDataCsReq,
|
||||||
|
GetPlayerBoardDataScRsp,
|
||||||
|
HeadIconData
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetPlayerBoardDataCsReq) -> betterproto.Message:
|
||||||
|
return GetPlayerBoardDataScRsp(
|
||||||
|
retcode=0,
|
||||||
|
current_head_icon_id=session.player.data.head_icon,
|
||||||
|
unlocked_head_icon_list=[
|
||||||
|
HeadIconData(
|
||||||
|
id=icon.ID
|
||||||
|
)
|
||||||
|
for icon in ResourceManager.instance().values(PlayerIconConfig)
|
||||||
|
]
|
||||||
|
)
|
||||||
26
game_server/handlers/GetPrivateChatHistoryCsReq.py
Normal file
26
game_server/handlers/GetPrivateChatHistoryCsReq.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetPrivateChatHistoryCsReq,
|
||||||
|
GetPrivateChatHistoryScRsp,
|
||||||
|
ChatMessageData,
|
||||||
|
MsgType
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetPrivateChatHistoryCsReq) -> betterproto.Message:
|
||||||
|
return GetPrivateChatHistoryScRsp(
|
||||||
|
retcode=0,
|
||||||
|
contact_side=msg.contact_side,
|
||||||
|
chat_message_list=[
|
||||||
|
ChatMessageData(
|
||||||
|
message_type=MsgType.MSG_TYPE_CUSTOM_TEXT.value,
|
||||||
|
content="No commands available. FUCK OFF!",
|
||||||
|
sender_id=69
|
||||||
|
),
|
||||||
|
ChatMessageData(
|
||||||
|
message_type=MsgType.MSG_TYPE_CUSTOM_TEXT.value,
|
||||||
|
content="Hi....",
|
||||||
|
sender_id=session.player.data.uid
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
58
game_server/handlers/GetSceneMapInfoCsReq.py
Normal file
58
game_server/handlers/GetSceneMapInfoCsReq.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.map_entrance import MapEntranceData
|
||||||
|
from game_server.resource.configdb.map_default_entrance import MapDefaultEntranceData
|
||||||
|
from rail_proto.lib import (
|
||||||
|
GetSceneMapInfoCsReq,
|
||||||
|
GetSceneMapInfoScRsp,
|
||||||
|
SceneMapInfo,
|
||||||
|
ChestInfo,
|
||||||
|
ChestType,
|
||||||
|
MazeGroup,
|
||||||
|
MazePropState
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: GetSceneMapInfoCsReq) -> betterproto.Message:
|
||||||
|
rsp = GetSceneMapInfoScRsp(retcode=0)
|
||||||
|
for floor_id in msg.floor_id_list:
|
||||||
|
map_default = ResourceManager.instance().find_by_index(MapDefaultEntranceData, floor_id)
|
||||||
|
if not map_default:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = ResourceManager.instance().find_by_index(MapEntranceData, map_default.EntranceID)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
map_info = SceneMapInfo(
|
||||||
|
retcode=0,
|
||||||
|
chest_list=[
|
||||||
|
ChestInfo(chest_type=ChestType.MAP_INFO_CHEST_TYPE_NORMAL.value),
|
||||||
|
ChestInfo(chest_type=ChestType.MAP_INFO_CHEST_TYPE_PUZZLE.value),
|
||||||
|
ChestInfo(chest_type=ChestType.MAP_INFO_CHEST_TYPE_CHALLENGE.value)
|
||||||
|
],
|
||||||
|
floor_id=floor_id
|
||||||
|
)
|
||||||
|
for i in range(101):
|
||||||
|
map_info.lighten_section_list.append(i)
|
||||||
|
|
||||||
|
level_group = data.floor_infos.get(map_default.EntranceID)
|
||||||
|
if level_group:
|
||||||
|
for group_id,group in level_group.groups.items():
|
||||||
|
maze_group_list = MazeGroup(
|
||||||
|
group_id=group_id
|
||||||
|
)
|
||||||
|
map_info.maze_group_list.append(maze_group_list)
|
||||||
|
for prop in group.PropList:
|
||||||
|
maze_prop = MazePropState(
|
||||||
|
group_id=group_id,
|
||||||
|
state=8 if prop.MappingInfoID > 0 else (1 if prop.State == 0 else prop.State),
|
||||||
|
config_id=prop.ID
|
||||||
|
)
|
||||||
|
if prop.MappingInfoID > 0:
|
||||||
|
map_info.unlock_teleport_list.append(prop.MappingInfoID)
|
||||||
|
map_info.maze_prop_list.append(maze_prop)
|
||||||
|
|
||||||
|
rsp.scene_map_info.append(map_info)
|
||||||
|
return rsp
|
||||||
37
game_server/handlers/PVEBattleResultCsReq.py
Normal file
37
game_server/handlers/PVEBattleResultCsReq.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
PVEBattleResultCsReq,
|
||||||
|
PVEBattleResultScRsp,
|
||||||
|
BattleEndStatus,
|
||||||
|
SceneGroupRefreshScNotify,
|
||||||
|
GroupRefreshInfo,
|
||||||
|
SceneGroupRefreshType,
|
||||||
|
SceneEntityRefreshInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: PVEBattleResultCsReq) -> betterproto.Message:
|
||||||
|
if msg.end_status == BattleEndStatus.BATTLE_END_WIN.value:
|
||||||
|
await session.notify(
|
||||||
|
SceneGroupRefreshScNotify(
|
||||||
|
group_refresh_list=[
|
||||||
|
GroupRefreshInfo(
|
||||||
|
refresh_type=SceneGroupRefreshType.SCENE_GROUP_REFRESH_TYPE_LOADED.value,
|
||||||
|
refresh_entity=[
|
||||||
|
SceneEntityRefreshInfo(
|
||||||
|
delete_entity=monster.entity_id
|
||||||
|
)
|
||||||
|
],
|
||||||
|
group_id=monster.group_id
|
||||||
|
) for monster in session.player.scene_manager.battle_monster
|
||||||
|
],
|
||||||
|
floor_id=session.player.data.floor_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for monster in session.player.scene_manager.battle_monster:
|
||||||
|
session.player.scene_manager.entities.pop(monster.entity_id, None)
|
||||||
|
return PVEBattleResultScRsp(
|
||||||
|
retcode=0,
|
||||||
|
end_status=msg.end_status,
|
||||||
|
battle_id=msg.battle_id
|
||||||
|
)
|
||||||
36
game_server/handlers/PlayerGetTokenCsReq.py
Normal file
36
game_server/handlers/PlayerGetTokenCsReq.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from database.account.account_data import find_account_by_uid
|
||||||
|
from database.player.player_data import PlayerData,find_player_by_uid
|
||||||
|
from database.lineup.lineup_data import LineupData
|
||||||
|
from rail_proto.lib import (
|
||||||
|
PlayerGetTokenCsReq,
|
||||||
|
PlayerGetTokenScRsp
|
||||||
|
)
|
||||||
|
from game_server.game.player.player_manager import PlayerManager
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: PlayerGetTokenCsReq) -> betterproto.Message:
|
||||||
|
account_uid = int(msg.account_uid)
|
||||||
|
account_data=find_account_by_uid(account_uid)
|
||||||
|
if not account_data:
|
||||||
|
return PlayerGetTokenScRsp(
|
||||||
|
retcode=1003
|
||||||
|
)
|
||||||
|
|
||||||
|
player_data=find_player_by_uid(account_uid)
|
||||||
|
if not player_data:
|
||||||
|
player_data=PlayerData(uid=account_uid).create_player_data(account_data.username)
|
||||||
|
session.player=PlayerManager(data=player_data)
|
||||||
|
session.player.add_avatar(8001)
|
||||||
|
session.player.add_avatar(1001)
|
||||||
|
session.player.add_lineup([8001])
|
||||||
|
|
||||||
|
else:
|
||||||
|
session.player=PlayerManager(data=player_data)
|
||||||
|
|
||||||
|
|
||||||
|
return PlayerGetTokenScRsp(
|
||||||
|
retcode=0,
|
||||||
|
uid=account_uid,
|
||||||
|
msg="OK"
|
||||||
|
)
|
||||||
17
game_server/handlers/PlayerHeartBeatCsReq.py
Normal file
17
game_server/handlers/PlayerHeartBeatCsReq.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import betterproto
|
||||||
|
import base64
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from utils.time import cur_timestamp_ms
|
||||||
|
from rail_proto.lib import (
|
||||||
|
PlayerHeartBeatCsReq,
|
||||||
|
PlayerHeartBeatScRsp,
|
||||||
|
ClientDownloadData
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: PlayerHeartBeatCsReq) -> betterproto.Message:
|
||||||
|
return PlayerHeartBeatScRsp(
|
||||||
|
retcode=0,
|
||||||
|
client_time_ms=msg.client_time_ms,
|
||||||
|
server_time_ms=cur_timestamp_ms(),
|
||||||
|
download_data=ClientDownloadData()
|
||||||
|
)
|
||||||
30
game_server/handlers/PlayerLoginCsReq.py
Normal file
30
game_server/handlers/PlayerLoginCsReq.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from utils.time import cur_timestamp_ms
|
||||||
|
from rail_proto.lib import (
|
||||||
|
PlayerLoginCsReq,
|
||||||
|
PlayerLoginScRsp,
|
||||||
|
PlayerBasicInfo
|
||||||
|
)
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: PlayerLoginCsReq) -> betterproto.Message:
|
||||||
|
try:
|
||||||
|
session.player.init_default()
|
||||||
|
return PlayerLoginScRsp(
|
||||||
|
login_random=msg.login_random,
|
||||||
|
server_timestamp_ms=cur_timestamp_ms(),
|
||||||
|
stamina=240,
|
||||||
|
basic_info=PlayerBasicInfo(
|
||||||
|
nickname=session.player.data.name,
|
||||||
|
level=session.player.data.level,
|
||||||
|
exp=session.player.data.exp,
|
||||||
|
stamina=240,
|
||||||
|
mcoin=session.player.data.mcoin,
|
||||||
|
hcoin=session.player.data.hcoin,
|
||||||
|
scoin=session.player.data.scoin,
|
||||||
|
world_level=session.player.data.world_level,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
28
game_server/handlers/PlayerLoginFinishCsReq.py
Normal file
28
game_server/handlers/PlayerLoginFinishCsReq.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
PlayerLoginFinishCsReq,
|
||||||
|
PlayerLoginFinishScRsp,
|
||||||
|
ContentPackageSyncDataScNotify,
|
||||||
|
ContentPackageData,
|
||||||
|
ContentPackageInfo,
|
||||||
|
ContentPackageStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: PlayerLoginFinishCsReq) -> betterproto.Message:
|
||||||
|
content = [200001,200002,200003,150017,150015]
|
||||||
|
await session.notify(
|
||||||
|
ContentPackageSyncDataScNotify(
|
||||||
|
data=ContentPackageData(
|
||||||
|
cur_content_id=0,
|
||||||
|
content_package_list=[
|
||||||
|
ContentPackageInfo(
|
||||||
|
content_id=id,
|
||||||
|
status=ContentPackageStatus.ContentPackageStatus_Finished.value
|
||||||
|
) for id in content
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session.active = True
|
||||||
|
return PlayerLoginFinishScRsp(retcode=0)
|
||||||
12
game_server/handlers/RelicRecommendCsReq.py
Normal file
12
game_server/handlers/RelicRecommendCsReq.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
RelicRecommendCsReq,
|
||||||
|
RelicRecommendScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: RelicRecommendCsReq) -> betterproto.Message:
|
||||||
|
return RelicRecommendScRsp(
|
||||||
|
retcode=0,
|
||||||
|
avatar_id=msg.avatar_id
|
||||||
|
)
|
||||||
18
game_server/handlers/ReplaceLineupCsReq.py
Normal file
18
game_server/handlers/ReplaceLineupCsReq.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
ReplaceLineupCsReq,
|
||||||
|
ReplaceLineupScRsp,
|
||||||
|
SyncLineupNotify
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: ReplaceLineupCsReq) -> betterproto.Message:
|
||||||
|
lineup = session.player.lineup_manager.get(msg.index)
|
||||||
|
lineup.avatar_list = [avatar.id for avatar in msg.lineup_slot_list]
|
||||||
|
await session.notify(
|
||||||
|
SyncLineupNotify(
|
||||||
|
lineup=lineup.ToProto()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
#lineup.save_lineup()
|
||||||
|
return ReplaceLineupScRsp(retcode=0)
|
||||||
63
game_server/handlers/SceneCastSkillCsReq.py
Normal file
63
game_server/handlers/SceneCastSkillCsReq.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
from game_server.resource.configdb.stage_config import StageConfig
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SceneCastSkillCsReq,
|
||||||
|
SceneCastSkillScRsp,
|
||||||
|
SceneBattleInfo,
|
||||||
|
SceneMonsterWave,
|
||||||
|
SceneMonster
|
||||||
|
)
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: SceneCastSkillCsReq) -> betterproto.Message:
|
||||||
|
targets = [id for id in msg.assist_monster_entity_id_list if id > 20000 or id < 10]
|
||||||
|
for monster in msg.assist_monster_entity_info:
|
||||||
|
targets.extend(
|
||||||
|
id for id in monster.entity_id_list
|
||||||
|
if id not in targets and (id > 20000 or id < 10)
|
||||||
|
)
|
||||||
|
if msg.skill_index == 0 and targets:
|
||||||
|
monster_data: list[StageConfig] = []
|
||||||
|
for entity_id in targets:
|
||||||
|
entity_data = session.player.scene_manager.entities.get(entity_id)
|
||||||
|
if entity_data:
|
||||||
|
stage = ResourceManager.instance().find_by_index(StageConfig,entity_data.npc_monster.event_id*10)
|
||||||
|
if not stage:
|
||||||
|
stage = ResourceManager.instance().find_by_index(StageConfig,entity_data.npc_monster.event_id)
|
||||||
|
if stage:
|
||||||
|
monster_data.append(stage)
|
||||||
|
session.player.scene_manager.battle_monster.append(entity_data)
|
||||||
|
if monster_data:
|
||||||
|
return SceneCastSkillScRsp(
|
||||||
|
retcode=0,
|
||||||
|
cast_entity_id=msg.cast_entity_id,
|
||||||
|
battle_info=SceneBattleInfo(
|
||||||
|
rounds_limit=0,
|
||||||
|
world_level=session.player.data.world_level,
|
||||||
|
logic_random_seed=np.random.randint(1, np.iinfo(np.int32).max, dtype=np.int32),
|
||||||
|
stage_id=monster_data[0].StageID,
|
||||||
|
battle_id=1,
|
||||||
|
battle_avatar_list=[
|
||||||
|
session.player.avatar_mananger.get(avatar_id).ToBattleProto(index,session)
|
||||||
|
for index, avatar_id in enumerate(session.player.lineup_manager.get(session.player.data.cur_lineup, None).avatar_list)
|
||||||
|
],
|
||||||
|
monster_wave_list=[
|
||||||
|
SceneMonsterWave(
|
||||||
|
battle_stage_id=monster_list.StageID,
|
||||||
|
battle_wave_id=index,
|
||||||
|
monster_list=[
|
||||||
|
SceneMonster(
|
||||||
|
monster_id=id
|
||||||
|
)
|
||||||
|
for monster_dict in monster_list.MonsterList
|
||||||
|
for monster, id in monster_dict.items()
|
||||||
|
if id > 0
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for index,monster_list in enumerate(monster_data)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return SceneCastSkillScRsp(retcode=0)
|
||||||
14
game_server/handlers/SelectChatBubbleCsReq.py
Normal file
14
game_server/handlers/SelectChatBubbleCsReq.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SelectChatBubbleCsReq,
|
||||||
|
SelectChatBubbleScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: SelectChatBubbleCsReq) -> betterproto.Message:
|
||||||
|
session.player.data.chat_bubble = msg.bubble_id
|
||||||
|
#session.player.data.save_player_data()
|
||||||
|
return SelectChatBubbleScRsp(
|
||||||
|
retcode=0,
|
||||||
|
cur_chat_bubble=msg.bubble_id
|
||||||
|
)
|
||||||
14
game_server/handlers/SelectPhoneThemeCsReq.py
Normal file
14
game_server/handlers/SelectPhoneThemeCsReq.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SelectPhoneThemeCsReq,
|
||||||
|
SelectPhoneThemeScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: SelectPhoneThemeCsReq) -> betterproto.Message:
|
||||||
|
session.player.data.chat_bubble = msg.theme_id
|
||||||
|
#session.player.data.save_player_data()
|
||||||
|
return SelectPhoneThemeScRsp(
|
||||||
|
retcode=0,
|
||||||
|
cur_phone_theme=msg.theme_id
|
||||||
|
)
|
||||||
36
game_server/handlers/SendMsgCsReq.py
Normal file
36
game_server/handlers/SendMsgCsReq.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from game_server.game.chat.command_handler import handler
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SendMsgCsReq,
|
||||||
|
SendMsgScRsp,
|
||||||
|
RevcMsgScNotify,
|
||||||
|
MsgType,
|
||||||
|
ChatType
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: SendMsgCsReq) -> betterproto.Message:
|
||||||
|
if msg.message_type == MsgType.MSG_TYPE_CUSTOM_TEXT:
|
||||||
|
session.pending_notify(
|
||||||
|
RevcMsgScNotify(
|
||||||
|
message_type=MsgType.MSG_TYPE_CUSTOM_TEXT.value,
|
||||||
|
chat_type=ChatType.CHAT_TYPE_PRIVATE.value,
|
||||||
|
message_text=msg.message_text,
|
||||||
|
target_uid=69,
|
||||||
|
source_uid=session.player.data.uid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
text = await handler.handle_command(session, msg.message_text)
|
||||||
|
if text:
|
||||||
|
session.pending_notify(
|
||||||
|
RevcMsgScNotify(
|
||||||
|
message_type=MsgType.MSG_TYPE_CUSTOM_TEXT.value,
|
||||||
|
chat_type=ChatType.CHAT_TYPE_PRIVATE.value,
|
||||||
|
message_text=text,
|
||||||
|
target_uid=session.player.data.uid,
|
||||||
|
source_uid=69
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return SendMsgScRsp(
|
||||||
|
retcode=0
|
||||||
|
)
|
||||||
12
game_server/handlers/SetClientPausedCsReq.py
Normal file
12
game_server/handlers/SetClientPausedCsReq.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SetClientPausedCsReq,
|
||||||
|
SetClientPausedScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: SetClientPausedCsReq) -> betterproto.Message:
|
||||||
|
return SetClientPausedScRsp(
|
||||||
|
retcode=0,
|
||||||
|
paused=msg.paused
|
||||||
|
)
|
||||||
14
game_server/handlers/SetHeadIconCsReq.py
Normal file
14
game_server/handlers/SetHeadIconCsReq.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SetHeadIconCsReq,
|
||||||
|
SetHeadIconScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: SetHeadIconCsReq) -> betterproto.Message:
|
||||||
|
session.player.data.head_icon = msg.id
|
||||||
|
#session.player.data.save_player_data()
|
||||||
|
return SetHeadIconScRsp(
|
||||||
|
retcode=0,
|
||||||
|
current_head_icon_id=msg.id
|
||||||
|
)
|
||||||
15
game_server/handlers/SetNicknameCsReq.py
Normal file
15
game_server/handlers/SetNicknameCsReq.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import betterproto
|
||||||
|
from game_server.net.session import PlayerSession
|
||||||
|
from rail_proto.lib import (
|
||||||
|
SetNicknameCsReq,
|
||||||
|
SetNicknameScRsp
|
||||||
|
)
|
||||||
|
|
||||||
|
async def handle(session: PlayerSession, msg: SetNicknameCsReq) -> betterproto.Message:
|
||||||
|
if msg.is_modify:
|
||||||
|
session.player.data.name = msg.nickname
|
||||||
|
await session.notify(session.player.PlayerSyncProto())
|
||||||
|
return SetNicknameScRsp(
|
||||||
|
retcode=0,
|
||||||
|
is_modify=msg.is_modify
|
||||||
|
)
|
||||||
12
game_server/main.py
Normal file
12
game_server/main.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import asyncio
|
||||||
|
from game_server.net.gateway import KCPGateway
|
||||||
|
from utils.config import Config
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
|
||||||
|
def fn_main():
|
||||||
|
try:
|
||||||
|
ResourceManager.instance().load_resources()
|
||||||
|
asyncio.run(KCPGateway.new(Config.GameServer.IP,Config.GameServer.Port))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
143
game_server/net/gateway.py
Normal file
143
game_server/net/gateway.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import asyncio
|
||||||
|
from nazo_rand import randrange
|
||||||
|
from game_server.net.kcp import get_conv
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class KCPGateway(asyncio.DatagramProtocol):
|
||||||
|
def __init__(self, db):
|
||||||
|
self.id_counter = 0
|
||||||
|
self.sessions : dict[int, PlayerSession] = {}
|
||||||
|
self.running = True
|
||||||
|
self.shutdown_event = asyncio.Event()
|
||||||
|
self.db = db
|
||||||
|
self.timeout_check_interval = 5
|
||||||
|
self.save_duration = 60
|
||||||
|
|
||||||
|
async def check_sessions_timeout(self):
|
||||||
|
while self.running:
|
||||||
|
for conv_id, session in list(self.sessions.items()):
|
||||||
|
if session.is_timeout():
|
||||||
|
self.drop_kcp_session(conv_id)
|
||||||
|
await asyncio.sleep(self.timeout_check_interval)
|
||||||
|
|
||||||
|
async def periodic_save_sessions(self):
|
||||||
|
while self.running:
|
||||||
|
start = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
for session in self.sessions.values():
|
||||||
|
session.player.save_all()
|
||||||
|
|
||||||
|
elapsed = asyncio.get_event_loop().time() - start
|
||||||
|
Info(f"Database saved in {elapsed:.2f}s")
|
||||||
|
|
||||||
|
await asyncio.sleep(self.save_duration)
|
||||||
|
|
||||||
|
def connection_made(self, transport):
|
||||||
|
self.transport = transport
|
||||||
|
Info(f"Listening on {self.transport.get_extra_info('sockname')}")
|
||||||
|
|
||||||
|
asyncio.create_task(self.check_sessions_timeout())
|
||||||
|
asyncio.create_task(self.periodic_save_sessions())
|
||||||
|
|
||||||
|
def datagram_received(self, data, addr):
|
||||||
|
data_len = len(data)
|
||||||
|
|
||||||
|
Debug(f"Received {data_len} bytes from {addr}: {[b for b in data]}")
|
||||||
|
|
||||||
|
if data_len == 20:
|
||||||
|
self.process_net_operation(NetOperation.from_bytes(data), addr)
|
||||||
|
elif data_len >= 28:
|
||||||
|
self.process_kcp_payload(data, addr)
|
||||||
|
else:
|
||||||
|
Warn("Unknown data length received")
|
||||||
|
|
||||||
|
def process_net_operation(self, op: NetOperation, addr):
|
||||||
|
if (op.head, op.tail) == (0xFF, 0xFFFFFFFF):
|
||||||
|
self.establish_kcp_session(op.data, addr)
|
||||||
|
elif (op.head, op.tail) == (0x194, 0x19419494):
|
||||||
|
self.drop_kcp_session(op.conv_id)
|
||||||
|
else:
|
||||||
|
Warn(f"Unknown magic pair: {op.head}-{op.tail}")
|
||||||
|
|
||||||
|
def establish_kcp_session(self, data, addr):
|
||||||
|
conv_id, session_token = self.next_conv_pair()
|
||||||
|
session_id = conv_id << 32 | session_token
|
||||||
|
|
||||||
|
Info(f"New connection: {addr} with conv_id: {conv_id}")
|
||||||
|
|
||||||
|
self.sessions[conv_id] = PlayerSession(
|
||||||
|
self.transport, session_id, addr, self.db
|
||||||
|
)
|
||||||
|
|
||||||
|
net_op = NetOperation(
|
||||||
|
head=0x145,
|
||||||
|
conv_id=conv_id,
|
||||||
|
session_token=session_token,
|
||||||
|
data=data,
|
||||||
|
tail=0x14514545,
|
||||||
|
).to_bytes()
|
||||||
|
|
||||||
|
self.transport.sendto(net_op, addr)
|
||||||
|
|
||||||
|
def drop_kcp_session(self, conv_id):
|
||||||
|
if conv_id in self.sessions:
|
||||||
|
self.sessions[conv_id].player.save_all()
|
||||||
|
del self.sessions[conv_id]
|
||||||
|
Info(f"Dropped KCP session with conv_id: {conv_id}")
|
||||||
|
else:
|
||||||
|
Warn(
|
||||||
|
f"Attempted to drop non-existent KCP session with conv_id: {conv_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_kcp_payload(self, data, addr):
|
||||||
|
conv_id = get_conv(data)
|
||||||
|
session = self.sessions.get(conv_id)
|
||||||
|
|
||||||
|
if session:
|
||||||
|
session.update_last_received()
|
||||||
|
asyncio.create_task(session.consume(data))
|
||||||
|
else:
|
||||||
|
Warn(f"Received data for unknown session conv_id: {conv_id}")
|
||||||
|
|
||||||
|
def next_conv_pair(self):
|
||||||
|
self.id_counter += 1
|
||||||
|
return self.id_counter, randrange(0, 0xFF)
|
||||||
|
|
||||||
|
def connection_lost(self, exc):
|
||||||
|
Info("UDP connection lost, shutting down...")
|
||||||
|
|
||||||
|
for session in self.sessions.values():
|
||||||
|
session.player.save_all()
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
self.shutdown_event.set()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
Info("Shutting down server...")
|
||||||
|
|
||||||
|
for session in self.sessions.values():
|
||||||
|
session.player.save_all()
|
||||||
|
|
||||||
|
self.running = False
|
||||||
|
self.shutdown_event.set()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def new(host, port):
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
transport, protocol = await loop.create_datagram_endpoint(
|
||||||
|
lambda: KCPGateway(db), local_addr=(host, port)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await protocol.shutdown_event.wait()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
Info("Server tasks cancelled.")
|
||||||
|
finally:
|
||||||
|
transport.close()
|
||||||
|
Info("Server stopped.")
|
||||||
657
game_server/net/kcp.py
Normal file
657
game_server/net/kcp.py
Normal file
@@ -0,0 +1,657 @@
|
|||||||
|
import struct
|
||||||
|
from collections import deque
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
# thanks mero for this kcp lib :3
|
||||||
|
# this file is excluded from the CC0-1.0 License
|
||||||
|
# this file is licensed under GPL-3.0 from the author written below
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "Mero <mero@crepe.moe>"
|
||||||
|
|
||||||
|
|
||||||
|
IKCP_RTO_NDL = 30
|
||||||
|
IKCP_RTO_MIN = 100
|
||||||
|
IKCP_RTO_DEF = 200
|
||||||
|
IKCP_RTO_MAX = 60000
|
||||||
|
IKCP_CMD_PUSH = 81
|
||||||
|
IKCP_CMD_ACK = 82
|
||||||
|
IKCP_CMD_WASK = 83
|
||||||
|
IKCP_CMD_WINS = 84
|
||||||
|
IKCP_ASK_SEND = 1
|
||||||
|
IKCP_ASK_TELL = 2
|
||||||
|
IKCP_WND_SND = 32
|
||||||
|
IKCP_WND_RCV = 128
|
||||||
|
IKCP_MTU_DEF = 1400
|
||||||
|
IKCP_ACK_FAST = 3
|
||||||
|
IKCP_INTERVAL = 100
|
||||||
|
IKCP_DEADLINK = 20
|
||||||
|
IKCP_THRESH_INIT = 2
|
||||||
|
IKCP_THRESH_MIN = 2
|
||||||
|
IKCP_PROBE_INIT = 7000
|
||||||
|
IKCP_PROBE_LIMIT = 120000
|
||||||
|
IKCP_FASTACK_LIMIT = 5
|
||||||
|
|
||||||
|
IKCP_PACKET_HEAD_FORMAT = "<IIBBHIIII"
|
||||||
|
IKCP_OVERHEAD = struct.calcsize(IKCP_PACKET_HEAD_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
def get_conv(buf: bytes) -> int:
|
||||||
|
assert len(buf) >= IKCP_OVERHEAD
|
||||||
|
return struct.unpack_from("<I", buf, 0)[0]
|
||||||
|
|
||||||
|
|
||||||
|
class KcpException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class KcpSegment:
|
||||||
|
__slots__ = (
|
||||||
|
"session_id",
|
||||||
|
"cmd",
|
||||||
|
"frg",
|
||||||
|
"wnd",
|
||||||
|
"ts",
|
||||||
|
"sn",
|
||||||
|
"una",
|
||||||
|
"data",
|
||||||
|
"resendts",
|
||||||
|
"rto",
|
||||||
|
"fastack",
|
||||||
|
"xmit",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.data = b""
|
||||||
|
|
||||||
|
self.resendts = 0
|
||||||
|
self.rto = 0
|
||||||
|
self.fastack = 0
|
||||||
|
self.xmit = 0
|
||||||
|
|
||||||
|
def parse(self, data):
|
||||||
|
conv, token, cmd, frg, wnd, ts, sn, una, len = struct.unpack(
|
||||||
|
IKCP_PACKET_HEAD_FORMAT, data[:IKCP_OVERHEAD]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session_id = conv << 32 | token
|
||||||
|
self.cmd = cmd
|
||||||
|
self.frg = frg
|
||||||
|
self.wnd = wnd
|
||||||
|
|
||||||
|
self.ts = ts
|
||||||
|
self.sn = sn
|
||||||
|
self.una = una
|
||||||
|
self.data = data[IKCP_OVERHEAD : IKCP_OVERHEAD + len]
|
||||||
|
|
||||||
|
return IKCP_OVERHEAD + len
|
||||||
|
|
||||||
|
def encode(self) -> bytes:
|
||||||
|
conv = self.session_id >> 32
|
||||||
|
token = self.session_id & 0xFFFFFFFF
|
||||||
|
|
||||||
|
return (
|
||||||
|
struct.pack(
|
||||||
|
IKCP_PACKET_HEAD_FORMAT,
|
||||||
|
conv,
|
||||||
|
token,
|
||||||
|
self.cmd,
|
||||||
|
self.frg,
|
||||||
|
self.wnd,
|
||||||
|
int(self.ts),
|
||||||
|
self.sn,
|
||||||
|
self.una,
|
||||||
|
int(len(self.data)),
|
||||||
|
)
|
||||||
|
+ self.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Kcp:
|
||||||
|
__slots__ = (
|
||||||
|
"session_id",
|
||||||
|
"current",
|
||||||
|
"rx_srtt",
|
||||||
|
"rx_rttval",
|
||||||
|
"snd_wnd",
|
||||||
|
"interval",
|
||||||
|
"rx_minrto",
|
||||||
|
"snd_nxt",
|
||||||
|
"rmt_wnd",
|
||||||
|
"snd_buf",
|
||||||
|
"snd_una",
|
||||||
|
"snd_queue",
|
||||||
|
"updated",
|
||||||
|
"ts_flush",
|
||||||
|
"xmit",
|
||||||
|
"state",
|
||||||
|
"ts_probe",
|
||||||
|
"probe_wait",
|
||||||
|
"use_fastask_conserve",
|
||||||
|
"rcv_nxt",
|
||||||
|
"rcv_wnd",
|
||||||
|
"rcv_buf",
|
||||||
|
"rcv_queue",
|
||||||
|
"probe",
|
||||||
|
"acklist",
|
||||||
|
"cwnd",
|
||||||
|
"mtu",
|
||||||
|
"mss",
|
||||||
|
"ssthresh",
|
||||||
|
"incr",
|
||||||
|
"rx_rto",
|
||||||
|
"stream",
|
||||||
|
"output",
|
||||||
|
"nodelay",
|
||||||
|
"nocwnd",
|
||||||
|
"dead_link",
|
||||||
|
"fastresend",
|
||||||
|
"fastlimit",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, session_id: int, output: Callable[[bytes], None]):
|
||||||
|
assert session_id < 1 << 64
|
||||||
|
|
||||||
|
self.use_fastask_conserve = False
|
||||||
|
self.session_id = session_id
|
||||||
|
self.output = output
|
||||||
|
|
||||||
|
self.snd_una = 0
|
||||||
|
self.snd_nxt = 0
|
||||||
|
self.rcv_nxt = 0
|
||||||
|
|
||||||
|
self.ts_probe = 0
|
||||||
|
self.probe_wait = 0
|
||||||
|
|
||||||
|
self.snd_wnd = IKCP_WND_SND
|
||||||
|
self.rcv_wnd = IKCP_WND_RCV
|
||||||
|
self.rmt_wnd = IKCP_WND_RCV
|
||||||
|
|
||||||
|
self.cwnd = 0
|
||||||
|
self.incr = 0
|
||||||
|
self.probe = 0
|
||||||
|
|
||||||
|
self.mtu = IKCP_MTU_DEF
|
||||||
|
self.mss = self.mtu - IKCP_OVERHEAD
|
||||||
|
self.stream = False
|
||||||
|
|
||||||
|
self.snd_buf = deque()
|
||||||
|
self.rcv_buf = deque()
|
||||||
|
self.rcv_queue = deque()
|
||||||
|
self.snd_queue = deque()
|
||||||
|
|
||||||
|
self.state = 0
|
||||||
|
self.acklist = deque()
|
||||||
|
|
||||||
|
self.rx_srtt = 0
|
||||||
|
self.rx_rttval = 0
|
||||||
|
self.rx_rto = IKCP_RTO_DEF
|
||||||
|
self.rx_minrto = IKCP_RTO_MIN
|
||||||
|
|
||||||
|
self.current = 0
|
||||||
|
self.interval = IKCP_INTERVAL
|
||||||
|
self.ts_flush = IKCP_INTERVAL
|
||||||
|
self.nodelay = 0
|
||||||
|
self.updated = False
|
||||||
|
|
||||||
|
self.ssthresh = IKCP_THRESH_INIT
|
||||||
|
self.fastresend = 0
|
||||||
|
self.fastlimit = IKCP_FASTACK_LIMIT
|
||||||
|
self.nocwnd = False
|
||||||
|
self.xmit = 0
|
||||||
|
self.dead_link = IKCP_DEADLINK
|
||||||
|
|
||||||
|
def parse_una(self, una):
|
||||||
|
while self.snd_buf:
|
||||||
|
seg = self.snd_buf[0]
|
||||||
|
|
||||||
|
if seg.sn >= una:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.snd_buf.popleft()
|
||||||
|
|
||||||
|
def shrink_buf(self):
|
||||||
|
self.snd_una = self.snd_buf[0].sn if self.snd_buf else self.snd_nxt
|
||||||
|
|
||||||
|
def update_ack(self, rtt):
|
||||||
|
if self.rx_srtt == 0:
|
||||||
|
self.rx_srtt = rtt
|
||||||
|
self.rx_rttval = rtt // 2
|
||||||
|
else:
|
||||||
|
delta = abs(rtt - self.rx_srtt)
|
||||||
|
self.rx_rttval = (3 * self.rx_rttval + delta) // 4
|
||||||
|
self.rx_srtt = max((7 * self.rx_srtt + rtt) // 8, 1)
|
||||||
|
|
||||||
|
rto = self.rx_srtt + max(self.interval, 4 * self.rx_rttval)
|
||||||
|
self.rx_rto = min(max(self.rx_minrto, rto), IKCP_RTO_MAX)
|
||||||
|
|
||||||
|
def parse_ack(self, sn):
|
||||||
|
if self.snd_una > sn or self.snd_nxt <= sn:
|
||||||
|
return
|
||||||
|
|
||||||
|
for seg in self.snd_buf:
|
||||||
|
if sn == seg.sn:
|
||||||
|
self.snd_buf.remove(seg)
|
||||||
|
break
|
||||||
|
|
||||||
|
if seg.sn > sn:
|
||||||
|
break
|
||||||
|
|
||||||
|
def move_buf(self):
|
||||||
|
while self.rcv_buf:
|
||||||
|
seg = self.rcv_buf[0]
|
||||||
|
if seg.sn != self.rcv_nxt or len(self.rcv_queue) >= self.rcv_wnd:
|
||||||
|
break
|
||||||
|
|
||||||
|
self.rcv_nxt += 1
|
||||||
|
self.rcv_queue.append(self.rcv_buf.popleft())
|
||||||
|
|
||||||
|
def parse_data(self, newseg):
|
||||||
|
if (self.rcv_nxt + self.rcv_wnd) <= newseg.sn or self.rcv_nxt > newseg.sn:
|
||||||
|
return
|
||||||
|
|
||||||
|
repeat = False
|
||||||
|
new_index = len(self.rcv_buf)
|
||||||
|
|
||||||
|
for seg in reversed(self.rcv_buf):
|
||||||
|
if seg.sn == newseg.sn:
|
||||||
|
repeat = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if seg.sn < newseg.sn:
|
||||||
|
break
|
||||||
|
|
||||||
|
new_index -= 1
|
||||||
|
|
||||||
|
if not repeat:
|
||||||
|
self.rcv_buf.insert(new_index, newseg)
|
||||||
|
|
||||||
|
self.move_buf()
|
||||||
|
|
||||||
|
def parse_fastack(self, sn, ts):
|
||||||
|
if self.snd_una > sn or self.snd_nxt <= sn:
|
||||||
|
return
|
||||||
|
|
||||||
|
for seg in self.snd_buf:
|
||||||
|
if seg.sn > sn:
|
||||||
|
break
|
||||||
|
elif sn != seg.sn and (self.use_fastask_conserve or seg.ts <= ts):
|
||||||
|
seg.fastack += 1
|
||||||
|
|
||||||
|
def input(self, data: bytes):
|
||||||
|
if not data or len(data) < IKCP_OVERHEAD:
|
||||||
|
raise KcpException(f"data size must be greater than {IKCP_OVERHEAD}")
|
||||||
|
|
||||||
|
maxack = 0
|
||||||
|
latest_ts = 0
|
||||||
|
flag = False
|
||||||
|
prev_una = self.snd_una
|
||||||
|
|
||||||
|
while len(data) >= IKCP_OVERHEAD:
|
||||||
|
seg = KcpSegment()
|
||||||
|
data = data[seg.parse(data) :]
|
||||||
|
|
||||||
|
if seg.session_id != self.session_id:
|
||||||
|
raise KcpException(
|
||||||
|
f"wrong session id, got {seg.session_id} but {self.session_id} was expected"
|
||||||
|
)
|
||||||
|
|
||||||
|
if seg.cmd not in (
|
||||||
|
IKCP_CMD_PUSH,
|
||||||
|
IKCP_CMD_ACK,
|
||||||
|
IKCP_CMD_WASK,
|
||||||
|
IKCP_CMD_WINS,
|
||||||
|
):
|
||||||
|
raise KcpException(f"unknown kcp cmd {seg.cmd}")
|
||||||
|
|
||||||
|
self.rmt_wnd = seg.wnd
|
||||||
|
|
||||||
|
self.parse_una(seg.una)
|
||||||
|
self.shrink_buf()
|
||||||
|
|
||||||
|
if seg.cmd == IKCP_CMD_ACK:
|
||||||
|
rtt = self.current - seg.ts
|
||||||
|
if rtt >= 0:
|
||||||
|
self.update_ack(rtt)
|
||||||
|
|
||||||
|
self.parse_ack(seg.sn)
|
||||||
|
self.shrink_buf()
|
||||||
|
|
||||||
|
if not flag:
|
||||||
|
flag = True
|
||||||
|
maxack = seg.sn
|
||||||
|
latest_ts = seg.ts
|
||||||
|
elif maxack < seg.sn and (
|
||||||
|
self.use_fastask_conserve or latest_ts > seg.ts
|
||||||
|
):
|
||||||
|
maxack = seg.sn
|
||||||
|
latest_ts = seg.ts
|
||||||
|
|
||||||
|
elif seg.cmd == IKCP_CMD_PUSH:
|
||||||
|
if self.rcv_nxt + self.rcv_wnd > seg.sn:
|
||||||
|
self.acklist.append((seg.sn, seg.ts))
|
||||||
|
if self.rcv_nxt <= seg.sn:
|
||||||
|
self.parse_data(seg)
|
||||||
|
|
||||||
|
elif seg.cmd == IKCP_CMD_WASK:
|
||||||
|
self.probe |= IKCP_ASK_TELL
|
||||||
|
|
||||||
|
if flag:
|
||||||
|
self.parse_fastack(maxack, latest_ts)
|
||||||
|
|
||||||
|
if self.snd_una - prev_una > 0 and self.cwnd < self.rmt_wnd:
|
||||||
|
mss = self.mss
|
||||||
|
if self.cwnd < self.ssthresh:
|
||||||
|
self.cwnd += 1
|
||||||
|
self.incr += mss
|
||||||
|
else:
|
||||||
|
if self.incr < mss:
|
||||||
|
self.incr = mss
|
||||||
|
self.incr += mss * mss // self.incr + mss // 16
|
||||||
|
if (self.cwnd + 1) * mss <= self.incr:
|
||||||
|
self.cwnd = (self.incr + mss - 1) // mss if mss > 0 else 1
|
||||||
|
if self.cwnd > self.rmt_wnd:
|
||||||
|
self.cwnd = self.rmt_wnd
|
||||||
|
self.incr = self.rmt_wnd * mss
|
||||||
|
|
||||||
|
def peeksize(self):
|
||||||
|
if not self.rcv_queue:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
seg = self.rcv_queue[0]
|
||||||
|
if seg.frg == 0:
|
||||||
|
return len(seg.data)
|
||||||
|
if len(self.rcv_queue) < seg.frg + 1:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
length = 0
|
||||||
|
for seg in self.rcv_queue:
|
||||||
|
length += len(seg.data)
|
||||||
|
if seg.frg == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
return length
|
||||||
|
|
||||||
|
def recv(self) -> bytes | None:
|
||||||
|
if not self.rcv_queue:
|
||||||
|
return None
|
||||||
|
|
||||||
|
peeksize = self.peeksize()
|
||||||
|
if peeksize < 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
recover = len(self.rcv_queue) >= self.rcv_wnd
|
||||||
|
data = b""
|
||||||
|
|
||||||
|
while seg := self.rcv_queue.popleft():
|
||||||
|
data += seg.data
|
||||||
|
if seg.frg == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
assert len(data) == peeksize
|
||||||
|
self.move_buf()
|
||||||
|
|
||||||
|
if len(self.rcv_queue) < self.rcv_wnd and recover:
|
||||||
|
self.probe |= IKCP_ASK_TELL
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def send(self, data: bytes):
|
||||||
|
assert self.mss > 0
|
||||||
|
|
||||||
|
if self.stream:
|
||||||
|
if self.snd_queue:
|
||||||
|
seg = self.snd_queue[-1]
|
||||||
|
if len(seg.data) < self.mss:
|
||||||
|
capacity = self.mss - len(seg.data)
|
||||||
|
extend = min(len(data), capacity)
|
||||||
|
|
||||||
|
seg.data += data[:extend]
|
||||||
|
data = data[extend:]
|
||||||
|
seg.frg = 0
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
|
||||||
|
count = (len(data) + self.mss - 1) // self.mss if len(data) > self.mss else 1
|
||||||
|
if count >= IKCP_WND_RCV:
|
||||||
|
raise KcpException("user buffer is too long")
|
||||||
|
count = max(count, 1)
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
size = min(self.mss, len(data))
|
||||||
|
|
||||||
|
newseg = KcpSegment()
|
||||||
|
newseg.data = data[:size]
|
||||||
|
newseg.frg = 0 if self.stream else count - i - 1
|
||||||
|
|
||||||
|
data = data[size:]
|
||||||
|
self.snd_queue.append(newseg)
|
||||||
|
|
||||||
|
def update(self, current: int):
|
||||||
|
assert current < 1 << 32
|
||||||
|
self.current = current
|
||||||
|
|
||||||
|
if not self.updated:
|
||||||
|
self.updated = True
|
||||||
|
self.ts_flush = self.current
|
||||||
|
|
||||||
|
slap = self.current - self.ts_flush
|
||||||
|
|
||||||
|
if slap >= 10000 or slap < -10000:
|
||||||
|
self.ts_flush = self.current
|
||||||
|
slap = 0
|
||||||
|
|
||||||
|
if slap >= 0:
|
||||||
|
self.ts_flush += self.interval
|
||||||
|
if self.ts_flush <= self.current:
|
||||||
|
self.ts_flush = self.current + self.interval
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def wnd_unused(self):
|
||||||
|
return max(self.rcv_wnd - len(self.rcv_queue), 0)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if not self.updated:
|
||||||
|
return
|
||||||
|
|
||||||
|
seg = KcpSegment()
|
||||||
|
seg.session_id = self.session_id
|
||||||
|
seg.cmd = IKCP_CMD_ACK
|
||||||
|
seg.frg = 0
|
||||||
|
seg.wnd = self.wnd_unused()
|
||||||
|
seg.una = self.rcv_nxt
|
||||||
|
seg.sn = 0
|
||||||
|
seg.ts = 0
|
||||||
|
|
||||||
|
data = b""
|
||||||
|
for sn, ts in self.acklist:
|
||||||
|
if len(data) + IKCP_OVERHEAD > self.mtu:
|
||||||
|
self.output(data)
|
||||||
|
data = b""
|
||||||
|
seg.sn = sn
|
||||||
|
seg.ts = ts
|
||||||
|
data += seg.encode()
|
||||||
|
self.acklist.clear()
|
||||||
|
|
||||||
|
if self.rmt_wnd == 0:
|
||||||
|
if self.probe_wait == 0:
|
||||||
|
self.probe_wait = IKCP_PROBE_INIT
|
||||||
|
self.ts_probe = self.current + self.probe_wait
|
||||||
|
elif self.ts_probe <= self.current:
|
||||||
|
self.probe_wait = min(
|
||||||
|
self.probe_wait + max(self.probe_wait, IKCP_PROBE_INIT) // 2,
|
||||||
|
IKCP_PROBE_LIMIT,
|
||||||
|
)
|
||||||
|
self.ts_probe = self.current + self.probe_wait
|
||||||
|
self.probe |= IKCP_ASK_SEND
|
||||||
|
else:
|
||||||
|
self.ts_probe = 0
|
||||||
|
self.probe_wait = 0
|
||||||
|
|
||||||
|
if self.probe & IKCP_ASK_SEND:
|
||||||
|
seg.cmd = IKCP_CMD_WASK
|
||||||
|
if len(data) + IKCP_OVERHEAD > self.mtu:
|
||||||
|
self.output(data)
|
||||||
|
data = b""
|
||||||
|
data += seg.encode()
|
||||||
|
|
||||||
|
if self.probe & IKCP_ASK_TELL:
|
||||||
|
seg.cmd = IKCP_CMD_WINS
|
||||||
|
if len(data) + IKCP_OVERHEAD > self.mtu:
|
||||||
|
self.output(data)
|
||||||
|
data = b""
|
||||||
|
data += seg.encode()
|
||||||
|
|
||||||
|
self.probe = 0
|
||||||
|
|
||||||
|
cwnd = min(self.snd_wnd, self.rmt_wnd)
|
||||||
|
if not self.nocwnd:
|
||||||
|
cwnd = min(self.cwnd, cwnd)
|
||||||
|
|
||||||
|
while self.snd_una + cwnd > self.snd_nxt:
|
||||||
|
if not self.snd_queue:
|
||||||
|
break
|
||||||
|
|
||||||
|
newseg = self.snd_queue.popleft()
|
||||||
|
self.snd_buf.append(newseg)
|
||||||
|
|
||||||
|
newseg.session_id = self.session_id
|
||||||
|
newseg.cmd = IKCP_CMD_PUSH
|
||||||
|
newseg.wnd = seg.wnd
|
||||||
|
newseg.ts = self.current
|
||||||
|
|
||||||
|
newseg.sn = self.snd_nxt
|
||||||
|
self.snd_nxt += 1
|
||||||
|
|
||||||
|
newseg.una = self.rcv_nxt
|
||||||
|
newseg.resendts = self.current
|
||||||
|
newseg.rto = self.rx_rto
|
||||||
|
newseg.fastack = 0
|
||||||
|
newseg.xmit = 0
|
||||||
|
|
||||||
|
resent = 0xFFFFFFFF
|
||||||
|
if self.fastresend > 0:
|
||||||
|
resent = self.fastresend
|
||||||
|
|
||||||
|
rtomin = 0
|
||||||
|
if not self.nodelay:
|
||||||
|
rtomin = self.rx_rto >> 3
|
||||||
|
|
||||||
|
lost = False
|
||||||
|
change = False
|
||||||
|
|
||||||
|
for segment in self.snd_buf:
|
||||||
|
needsend = False
|
||||||
|
|
||||||
|
if segment.xmit == 0:
|
||||||
|
needsend = True
|
||||||
|
segment.xmit += 1
|
||||||
|
segment.rto = self.rx_rto
|
||||||
|
segment.resendts = self.current + segment.rto + rtomin
|
||||||
|
elif segment.resendts <= self.current:
|
||||||
|
needsend = True
|
||||||
|
segment.xmit += 1
|
||||||
|
self.xmit += 1
|
||||||
|
if not self.nodelay:
|
||||||
|
segment.rto += max(segment.rto, self.rx_rto)
|
||||||
|
else:
|
||||||
|
step = segment.rto if self.nodelay < 2 else self.rx_rto
|
||||||
|
segment.rto += step // 2
|
||||||
|
segment.resendts = self.current + segment.rto
|
||||||
|
lost = True
|
||||||
|
elif segment.fastack >= resent and (
|
||||||
|
segment.xmit <= self.fastlimit or self.fastlimit <= 0
|
||||||
|
):
|
||||||
|
needsend = True
|
||||||
|
segment.xmit += 1
|
||||||
|
segment.fastack = 0
|
||||||
|
segment.resendts = self.current + segment.rto
|
||||||
|
change = True
|
||||||
|
|
||||||
|
if needsend:
|
||||||
|
segment.ts = self.current
|
||||||
|
segment.wnd = seg.wnd
|
||||||
|
segment.una = self.rcv_nxt
|
||||||
|
|
||||||
|
if len(data) + IKCP_OVERHEAD + len(segment.data) > self.mtu:
|
||||||
|
self.output(data)
|
||||||
|
data = b""
|
||||||
|
|
||||||
|
data += segment.encode()
|
||||||
|
if segment.xmit >= self.dead_link:
|
||||||
|
self.state = -1
|
||||||
|
|
||||||
|
if data:
|
||||||
|
self.output(data)
|
||||||
|
|
||||||
|
if change:
|
||||||
|
inflight = self.snd_nxt - self.snd_una
|
||||||
|
self.ssthresh = max(inflight // 2, IKCP_THRESH_MIN)
|
||||||
|
self.cwnd = self.ssthresh + resent
|
||||||
|
self.incr = self.cwnd * self.mss
|
||||||
|
|
||||||
|
if lost:
|
||||||
|
self.ssthresh = max(cwnd // 2, IKCP_THRESH_MIN)
|
||||||
|
self.cwnd = 1
|
||||||
|
self.incr = self.mss
|
||||||
|
|
||||||
|
if self.cwnd < 1:
|
||||||
|
self.cwnd = 1
|
||||||
|
self.incr = self.mss
|
||||||
|
|
||||||
|
def check(self, current):
|
||||||
|
assert current < 1 << 32
|
||||||
|
|
||||||
|
ts_flush = self.ts_flush
|
||||||
|
tm_packet = 0x7FFFFFFF
|
||||||
|
|
||||||
|
if not self.updated:
|
||||||
|
return current
|
||||||
|
|
||||||
|
if current - self.ts_flush >= 10000 or current - self.ts_flush < -10000:
|
||||||
|
ts_flush = current
|
||||||
|
|
||||||
|
if ts_flush <= current:
|
||||||
|
return current
|
||||||
|
|
||||||
|
tm_flush = ts_flush - current
|
||||||
|
|
||||||
|
for seg in self.snd_buf:
|
||||||
|
diff = seg.resendts - current
|
||||||
|
if diff <= 0:
|
||||||
|
return current
|
||||||
|
if diff < tm_packet:
|
||||||
|
tm_packet = diff
|
||||||
|
|
||||||
|
return current + min(tm_flush, tm_packet, self.interval)
|
||||||
|
|
||||||
|
def set_mtu(self, mtu: int):
|
||||||
|
if mtu < 50 or mtu < IKCP_OVERHEAD:
|
||||||
|
raise KcpException("invalid mtu")
|
||||||
|
|
||||||
|
self.mtu = mtu
|
||||||
|
self.mss = self.mtu - IKCP_OVERHEAD
|
||||||
|
|
||||||
|
def set_nodelay(self, nodelay: int, interval: int, resend: int, nc: int):
|
||||||
|
if nodelay >= 0:
|
||||||
|
self.nodelay = nodelay
|
||||||
|
if nodelay:
|
||||||
|
self.rx_minrto = IKCP_RTO_NDL
|
||||||
|
else:
|
||||||
|
self.rx_minrto = IKCP_RTO_MIN
|
||||||
|
|
||||||
|
if interval >= 0:
|
||||||
|
self.interval = min(max(10, interval), 5000)
|
||||||
|
|
||||||
|
if resend >= 0:
|
||||||
|
self.fastresend = resend
|
||||||
|
|
||||||
|
if nc >= 0:
|
||||||
|
self.nocwnd = nc
|
||||||
|
|
||||||
|
def set_wndsize(self, sndwnd: int, rcvwnd: int):
|
||||||
|
if sndwnd > 0:
|
||||||
|
self.snd_wnd = sndwnd
|
||||||
|
if rcvwnd > 0:
|
||||||
|
self.rcv_wnd = max(rcvwnd, IKCP_WND_RCV)
|
||||||
93
game_server/net/packet.py
Normal file
93
game_server/net/packet.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
import struct
|
||||||
|
|
||||||
|
HEAD_MAGIC = 0x9D74C714
|
||||||
|
TAIL_MAGIC = 0xD7A152C8
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NetPacket:
|
||||||
|
cmd_type: int
|
||||||
|
head: bytes
|
||||||
|
body: bytes
|
||||||
|
|
||||||
|
def to_message(self, m) -> "NetPacket":
|
||||||
|
return m.parse(self.body)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_message(c, m) -> "NetPacket":
|
||||||
|
return NetPacket(cmd_type=c, head=[], body=bytes(m))
|
||||||
|
|
||||||
|
def to_bytes(self) -> bytes:
|
||||||
|
# packet_length = 12 + len(self.head) + len(self.body) + 4
|
||||||
|
b = bytearray()
|
||||||
|
|
||||||
|
b.extend(struct.pack(">I", HEAD_MAGIC))
|
||||||
|
b.extend(struct.pack(">H", self.cmd_type))
|
||||||
|
b.extend(struct.pack(">H", len(self.head)))
|
||||||
|
b.extend(struct.pack(">I", len(self.body)))
|
||||||
|
b.extend(self.head)
|
||||||
|
b.extend(self.body)
|
||||||
|
b.extend(struct.pack(">I", TAIL_MAGIC))
|
||||||
|
|
||||||
|
return bytes(b)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_bytes(b: bytes) -> "NetPacket":
|
||||||
|
if len(b) < 16:
|
||||||
|
raise ValueError("len(b) < 16")
|
||||||
|
|
||||||
|
head_magic = struct.unpack_from(">I", b, 0)[0]
|
||||||
|
|
||||||
|
if head_magic != HEAD_MAGIC:
|
||||||
|
raise ValueError("Invalid head magic")
|
||||||
|
|
||||||
|
cmd_type = struct.unpack_from(">H", b, 4)[0]
|
||||||
|
head_length = struct.unpack_from(">H", b, 6)[0]
|
||||||
|
body_length = struct.unpack_from(">I", b, 8)[0]
|
||||||
|
|
||||||
|
head_start = 12
|
||||||
|
head_end = head_start + head_length
|
||||||
|
|
||||||
|
if head_end > len(b):
|
||||||
|
raise ValueError("Head data > packet length")
|
||||||
|
|
||||||
|
head = b[head_start:head_end]
|
||||||
|
|
||||||
|
body_start = head_end
|
||||||
|
body_end = body_start + body_length
|
||||||
|
|
||||||
|
if body_end + 4 > len(b):
|
||||||
|
raise ValueError("Body data > packet length")
|
||||||
|
|
||||||
|
body = b[body_start:body_end]
|
||||||
|
|
||||||
|
tail_magic = struct.unpack_from(">I", b, body_end)[0]
|
||||||
|
|
||||||
|
if tail_magic != TAIL_MAGIC:
|
||||||
|
raise ValueError("Invalid tail magic")
|
||||||
|
|
||||||
|
return NetPacket(cmd_type, head, body)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NetOperation:
|
||||||
|
head: int
|
||||||
|
conv_id: int
|
||||||
|
session_token: int
|
||||||
|
data: int
|
||||||
|
tail: int
|
||||||
|
|
||||||
|
def to_bytes(self) -> bytes:
|
||||||
|
return struct.pack(
|
||||||
|
">IIIII", self.head, self.conv_id, self.session_token, self.data, self.tail
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_bytes(b: bytes) -> "NetOperation":
|
||||||
|
if len(b) != 20:
|
||||||
|
raise ValueError("len(b) != 20")
|
||||||
|
|
||||||
|
head, conv_id, session_token, data, tail = struct.unpack(">IIIII", b)
|
||||||
|
|
||||||
|
return NetOperation(head, conv_id, session_token, data, tail)
|
||||||
133
game_server/net/session.py
Normal file
133
game_server/net/session.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import asyncio
|
||||||
|
import importlib
|
||||||
|
import betterproto
|
||||||
|
from game_server.net.kcp import Kcp
|
||||||
|
from game_server.net.packet import NetPacket
|
||||||
|
from utils.logger import Info,Error,Warn
|
||||||
|
from rail_proto import cmd
|
||||||
|
from rail_proto import lib as protos
|
||||||
|
from game_server.game.player.player_manager import PlayerManager
|
||||||
|
from game_server.dummy import dummyprotolist
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
class PlayerSession:
|
||||||
|
def __init__(self, transport, session_id, client_addr, db):
|
||||||
|
self.transport = transport
|
||||||
|
self.session_id = session_id
|
||||||
|
self.client_addr = client_addr
|
||||||
|
self.kcp = Kcp(session_id, self.send_output)
|
||||||
|
self.kcp.set_nodelay(1, 5, 2, 0)
|
||||||
|
self.is_destroyed = False
|
||||||
|
self.db = db
|
||||||
|
self.pending_notifies = list()
|
||||||
|
self.player = PlayerManager()
|
||||||
|
self.active = False
|
||||||
|
self.last_received = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
def update_last_received(self):
|
||||||
|
self.last_received = asyncio.get_event_loop().time()
|
||||||
|
|
||||||
|
def is_timeout(self, timeout=15):
|
||||||
|
return asyncio.get_event_loop().time() - self.last_received > timeout
|
||||||
|
|
||||||
|
def pending_notify(self, data: betterproto.Message, delay=0):
|
||||||
|
"""
|
||||||
|
This can be used to queue packet to be sent after response inside a handler
|
||||||
|
"""
|
||||||
|
self.pending_notifies.append((data, delay))
|
||||||
|
|
||||||
|
async def notify(self, data: betterproto.Message):
|
||||||
|
msg_name = data.__class__.__name__
|
||||||
|
cmd_id = getattr(cmd.CmdID, msg_name, None)
|
||||||
|
if not cmd_id:
|
||||||
|
Warn(f"Server tried to send notify with unsupported message: {msg_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
response_packet = NetPacket.from_message(cmd_id, data)
|
||||||
|
await self.send(response_packet)
|
||||||
|
|
||||||
|
def send_output(self, data):
|
||||||
|
self.transport.sendto(data, self.client_addr)
|
||||||
|
|
||||||
|
async def consume(self, data):
|
||||||
|
self.kcp.input(data)
|
||||||
|
self.kcp.update(asyncio.get_running_loop().time())
|
||||||
|
|
||||||
|
while True:
|
||||||
|
packet = self.kcp.recv()
|
||||||
|
if packet is None:
|
||||||
|
break
|
||||||
|
await self.handle_packet(packet)
|
||||||
|
|
||||||
|
self.kcp.update(asyncio.get_running_loop().time())
|
||||||
|
|
||||||
|
async def handle_packet(self, packet):
|
||||||
|
net_packet = NetPacket.from_bytes(packet)
|
||||||
|
cmd_id = net_packet.cmd_type
|
||||||
|
|
||||||
|
request_name = cmd.get_key_by_value(cmd_id)
|
||||||
|
if not request_name:
|
||||||
|
Warn(
|
||||||
|
f"Request doesn't have registered message_id: {cmd_id}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if request_name[:-5] in dummyprotolist:
|
||||||
|
dummy_cmd_id = getattr(cmd.CmdID, f"{request_name[:-5]}ScRsp", None)
|
||||||
|
dummy_response = NetPacket.from_message(dummy_cmd_id, b'')
|
||||||
|
await self.send(dummy_response)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
req: betterproto.Message = getattr(protos, request_name)()
|
||||||
|
req.parse(net_packet.body)
|
||||||
|
except Exception:
|
||||||
|
req = betterproto.Message()
|
||||||
|
|
||||||
|
try:
|
||||||
|
handle_result: betterproto.Message = await importlib.import_module(
|
||||||
|
f"game_server.handlers.{request_name}"
|
||||||
|
).handle(self, req)
|
||||||
|
if not handle_result:
|
||||||
|
return
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
Error(f"Unhandled request {request_name}")
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
Error(f"Handler {request_name} returns error.")
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
Info(f"Received cmd: {request_name}({cmd_id})")
|
||||||
|
|
||||||
|
response_name = handle_result.__class__.__name__
|
||||||
|
cmd_type = getattr(cmd.CmdID, response_name, None)
|
||||||
|
if not cmd_type:
|
||||||
|
Warn(
|
||||||
|
f"Server tried to send response with unsupported message: {response_name}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
response_packet = NetPacket.from_message(cmd_type, handle_result)
|
||||||
|
await self.send(response_packet)
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.create_task(self.send_pending_notifies(
|
||||||
|
self.pending_notifies.copy()
|
||||||
|
))
|
||||||
|
|
||||||
|
self.pending_notifies.clear()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def send_pending_notifies(
|
||||||
|
self,
|
||||||
|
pending_notifies: list[tuple[betterproto.Message, int]]
|
||||||
|
):
|
||||||
|
for notify, delay in pending_notifies:
|
||||||
|
if delay > 0:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
await self.notify(notify)
|
||||||
|
|
||||||
|
async def send(self, packet):
|
||||||
|
self.kcp.send(packet.to_bytes())
|
||||||
|
self.kcp.flush()
|
||||||
|
self.kcp.update(asyncio.get_running_loop().time())
|
||||||
79
game_server/resource/__init__.py
Normal file
79
game_server/resource/__init__.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
from typing import Dict, Type, TypeVar, Optional, Any, List
|
||||||
|
|
||||||
|
from utils.logger import Error, Info
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import resource_registry
|
||||||
|
import game_server.resource.configdb # noqa: F401
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=BaseResource)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_data(cls: Type, data):
|
||||||
|
valid_fields = cls.__annotations__.keys()
|
||||||
|
return {field: data.get(field, None) for field in valid_fields}
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.data: Dict[Type[T], Dict[Any, T]] = {}
|
||||||
|
self.check_floor: bool = False
|
||||||
|
|
||||||
|
def load_resources(self) -> None:
|
||||||
|
Info("[BOOT] [ResourceManager] Loading Resourcses...")
|
||||||
|
sorted_load_priority = dict(
|
||||||
|
sorted(resource_registry.items(), key=lambda item: item[1]["load_priority"])
|
||||||
|
).items()
|
||||||
|
for cls, metadata in sorted_load_priority:
|
||||||
|
path = metadata["path"]
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as file:
|
||||||
|
raw_data = json.load(file)
|
||||||
|
except Exception:
|
||||||
|
Error(f"Error when loading resource {path}")
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.data[cls] = {}
|
||||||
|
i = 0
|
||||||
|
for data in raw_data:
|
||||||
|
data = filter_data(cls, data)
|
||||||
|
item: T = cls(**data)
|
||||||
|
if "MapEntrance" in path:
|
||||||
|
if not self.check_floor:
|
||||||
|
Info("[BOOT] Loading floor infos... this may take a while.")
|
||||||
|
self.check_floor = True
|
||||||
|
if not item.check_floor_dir():
|
||||||
|
break
|
||||||
|
if not item.on_load():
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
index_value = item.get_index()
|
||||||
|
|
||||||
|
if index_value not in self.data[cls]:
|
||||||
|
self.data[cls][index_value] = []
|
||||||
|
self.data[cls][index_value].append(item)
|
||||||
|
Info(f"[BOOT] [ResourceManager] Loaded {i} config(s) for {cls.__name__}")
|
||||||
|
if "MapEntrance" in path:
|
||||||
|
Info(f"[BOOT] Loaded floor infos.")
|
||||||
|
|
||||||
|
def find_by_index(self, cls: Type[T], value: Any) -> Optional[T]:
|
||||||
|
items = self.data.get(cls, {}).get(str(value), [])
|
||||||
|
return items[0] if items else None
|
||||||
|
|
||||||
|
def find_all_by_index(self, cls: Type[T], value: Any) -> List[T]:
|
||||||
|
return self.data.get(cls, {}).get(str(value), [])
|
||||||
|
|
||||||
|
def has_index(self, cls: Type[T], value: Any) -> bool:
|
||||||
|
return str(value) in self.data.get(cls, {})
|
||||||
|
|
||||||
|
def values(self, cls: Type[T]) -> List[T]:
|
||||||
|
return [item for sublist in self.data.get(cls, {}).values() for item in sublist]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def instance():
|
||||||
|
return resource_manager
|
||||||
|
|
||||||
|
|
||||||
|
resource_manager = ResourceManager()
|
||||||
19
game_server/resource/base_resource.py
Normal file
19
game_server/resource/base_resource.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="BaseResource")
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BaseResource(ABC):
|
||||||
|
def on_load(self: T) -> bool:
|
||||||
|
"""returns True by default if item loaded, otherwise will be skipped"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_floor_dir(self: T) -> bool:
|
||||||
|
"""returns True by default if floor dir exist, otherwise will be skipped"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_index(self) -> str:
|
||||||
|
pass
|
||||||
15
game_server/resource/configdb/__init__.py
Normal file
15
game_server/resource/configdb/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
folder = "game_server/resource/configdb"
|
||||||
|
sys.path.append(os.path.dirname(folder))
|
||||||
|
|
||||||
|
for filename in os.listdir(folder):
|
||||||
|
if filename.endswith(".py") and filename != "__init__.py":
|
||||||
|
module_name = filename[:-3]
|
||||||
|
module_path = f"game_server.resource.configdb.{module_name}"
|
||||||
|
try:
|
||||||
|
importlib.import_module(module_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error importing module '{module_path}': {e}")
|
||||||
32
game_server/resource/configdb/avatar_config.py
Normal file
32
game_server/resource/configdb/avatar_config.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from game_server.resource.base_resource import BaseResource, T
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
from game_server.resource.configdb.avatar_skill_tree_config import AvatarSkillTreeConfig
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/AvatarConfig.json")
|
||||||
|
class AvatarConfig(BaseResource):
|
||||||
|
AvatarID: int
|
||||||
|
AvatarSkills: dict[int, int]
|
||||||
|
|
||||||
|
def on_load(self: T) -> bool:
|
||||||
|
from game_server.resource import ResourceManager
|
||||||
|
|
||||||
|
self.AvatarSkills = {}
|
||||||
|
|
||||||
|
skill_tree_config = ResourceManager.instance().find_all_by_index(
|
||||||
|
AvatarSkillTreeConfig, self.AvatarID
|
||||||
|
)
|
||||||
|
|
||||||
|
for skill in skill_tree_config:
|
||||||
|
skill_id = skill.PointID
|
||||||
|
skill_level = skill.MaxLevel
|
||||||
|
|
||||||
|
skill = self.AvatarSkills.get(skill_id)
|
||||||
|
if not skill:
|
||||||
|
self.AvatarSkills[skill_id] = skill_level
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.AvatarID)
|
||||||
13
game_server/resource/configdb/avatar_skill_tree_config.py
Normal file
13
game_server/resource/configdb/avatar_skill_tree_config.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource, LoadPriority
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/AvatarSkillTreeConfig.json", load_priority=LoadPriority.HIGH)
|
||||||
|
class AvatarSkillTreeConfig(BaseResource):
|
||||||
|
AvatarID: int
|
||||||
|
MaxLevel: int
|
||||||
|
PointID: int
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.AvatarID)
|
||||||
22
game_server/resource/configdb/challenge_maze_config.py
Normal file
22
game_server/resource/configdb/challenge_maze_config.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/ChallengeMazeConfig.json")
|
||||||
|
class ChallengeMazeConfig(BaseResource):
|
||||||
|
ID: int
|
||||||
|
MazeBuffID: int
|
||||||
|
ChallengeCountDown: int
|
||||||
|
MapEntranceID: int
|
||||||
|
MapEntranceID2: int
|
||||||
|
MazeGroupID1: int
|
||||||
|
MazeGroupID2: int
|
||||||
|
GroupID: int
|
||||||
|
EventIDList1: list
|
||||||
|
EventIDList2: list
|
||||||
|
NpcMonsterIDList1: list
|
||||||
|
NpcMonsterIDList2: list
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.ID)
|
||||||
12
game_server/resource/configdb/equipment_config.py
Normal file
12
game_server/resource/configdb/equipment_config.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/EquipmentConfig.json")
|
||||||
|
class EquipmentConfig(BaseResource):
|
||||||
|
EquipmentID: int
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.EquipmentID)
|
||||||
13
game_server/resource/configdb/item_config.py
Normal file
13
game_server/resource/configdb/item_config.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/ItemConfig.json")
|
||||||
|
class ItemConfig(BaseResource):
|
||||||
|
ID: int
|
||||||
|
ItemSubType: str
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.ID)
|
||||||
11
game_server/resource/configdb/main_mission.py
Normal file
11
game_server/resource/configdb/main_mission.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/MainMission.json")
|
||||||
|
class MainMissionData(BaseResource):
|
||||||
|
MainMissionID: int
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.MainMissionID)
|
||||||
12
game_server/resource/configdb/map_default_entrance.py
Normal file
12
game_server/resource/configdb/map_default_entrance.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/MapDefaultEntrance.json")
|
||||||
|
class MapDefaultEntranceData(BaseResource):
|
||||||
|
EntranceID: int
|
||||||
|
FloorID: int
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.FloorID)
|
||||||
85
game_server/resource/configdb/map_entrance.py
Normal file
85
game_server/resource/configdb/map_entrance.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import dataclasses
|
||||||
|
from utils.logger import Warn
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource,LoadPriority
|
||||||
|
from game_server.resource.configdb.sub_type.floor_info import FloorInfo,parse_floor_info
|
||||||
|
from game_server.resource.configdb.sub_type.group_info import parse_group_info
|
||||||
|
|
||||||
|
floor_dir = './resources/Config/LevelOutput/RuntimeFloor/'
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/MapEntrance.json", load_priority=LoadPriority.LOW)
|
||||||
|
class MapEntranceData(BaseResource):
|
||||||
|
ID: int
|
||||||
|
PlaneID: int
|
||||||
|
FloorID: int
|
||||||
|
StartAnchorID: int
|
||||||
|
StartGroupID: int
|
||||||
|
EntranceType: str
|
||||||
|
|
||||||
|
floor_infos: dict[int, FloorInfo] = dataclasses.field(default_factory=dict)
|
||||||
|
|
||||||
|
def on_load(self) -> bool:
|
||||||
|
name = f"P{self.PlaneID}_F{self.FloorID}"
|
||||||
|
file_path = os.path.join(floor_dir, f"{name}.json")
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
Warn(f"Missing floor info: {name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
raw_data = json.load(file)
|
||||||
|
|
||||||
|
self.floor_infos = {}
|
||||||
|
|
||||||
|
floor_data = parse_floor_info(raw_data)
|
||||||
|
self.floor_infos[self.ID] = floor_data
|
||||||
|
|
||||||
|
floor = self.floor_infos[self.ID]
|
||||||
|
|
||||||
|
for simpleGroup in floor.GroupInstanceList:
|
||||||
|
if simpleGroup.IsDelete:
|
||||||
|
continue
|
||||||
|
|
||||||
|
group_file_path = os.path.join('./resources/', simpleGroup.GroupPath)
|
||||||
|
|
||||||
|
if not os.path.exists(group_file_path):
|
||||||
|
Warn(f"Missing GroupPath info: {simpleGroup.GroupPath}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(group_file_path, 'r', encoding='utf-8') as group_file:
|
||||||
|
content = group_file.read().strip()
|
||||||
|
if not content:
|
||||||
|
Warn(f"File is empty: {group_file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
group_raw_data = json.loads(content)
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
Warn(f"Error decoding JSON in file {group_file_path}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Warn(f"Unexpected error occurred while processing file {group_file_path}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
group_info = parse_group_info(group_raw_data)
|
||||||
|
group_info.id = simpleGroup.ID
|
||||||
|
|
||||||
|
if group_info.OwnerMainMissionID and group_info.OwnerMainMissionID > 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
floor.groups[simpleGroup.ID] = group_info
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_floor_dir(self) -> bool:
|
||||||
|
if not os.path.exists(floor_dir):
|
||||||
|
Warn(f"Floor infos are missing, please check your resources folder: {floor_dir}. Teleports and natural world spawns may not work!")
|
||||||
|
return
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.ID)
|
||||||
15
game_server/resource/configdb/maze_plane.py
Normal file
15
game_server/resource/configdb/maze_plane.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
from game_server.game.enums.scene.game_mode_type import GameModeTypeEnum
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/MazePlane.json")
|
||||||
|
class MazePlaneData(BaseResource):
|
||||||
|
PlaneID: int
|
||||||
|
PlaneType: GameModeTypeEnum
|
||||||
|
WorldID: int
|
||||||
|
StartFloorID: int
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.PlaneID)
|
||||||
13
game_server/resource/configdb/player_icon_config.py
Normal file
13
game_server/resource/configdb/player_icon_config.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/PlayerIconConfig.json")
|
||||||
|
class PlayerIconConfig(BaseResource):
|
||||||
|
ID: int
|
||||||
|
IsVisible: bool
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.ID)
|
||||||
18
game_server/resource/configdb/relic_config.py
Normal file
18
game_server/resource/configdb/relic_config.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/RelicConfig.json")
|
||||||
|
class RelicConfigData(BaseResource):
|
||||||
|
ID: int
|
||||||
|
Type: str
|
||||||
|
MaxLevel: int
|
||||||
|
MainAffixGroup: int
|
||||||
|
SubAffixGroup: int
|
||||||
|
|
||||||
|
def on_load(self) -> bool:
|
||||||
|
return self.MaxLevel == 15
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.ID)
|
||||||
15
game_server/resource/configdb/relic_main_affix_config.py
Normal file
15
game_server/resource/configdb/relic_main_affix_config.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/RelicMainAffixConfig.json")
|
||||||
|
class RelicMainAffixConfigData(BaseResource):
|
||||||
|
GroupID: int
|
||||||
|
AffixID: int
|
||||||
|
Property: str
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.GroupID)
|
||||||
|
|
||||||
|
|
||||||
16
game_server/resource/configdb/relic_sub_affix_config.py
Normal file
16
game_server/resource/configdb/relic_sub_affix_config.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/RelicSubAffixConfig.json")
|
||||||
|
class RelicSubAffixConfigData(BaseResource):
|
||||||
|
GroupID: int
|
||||||
|
AffixID: int
|
||||||
|
StepNum: int
|
||||||
|
Property: str
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.GroupID)
|
||||||
|
|
||||||
|
|
||||||
15
game_server/resource/configdb/stage_config.py
Normal file
15
game_server/resource/configdb/stage_config.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from game_server.resource.base_resource import BaseResource
|
||||||
|
from game_server.resource.decorators import GameResource
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
@GameResource("resources/ExcelOutput/StageConfig.json")
|
||||||
|
class StageConfig(BaseResource):
|
||||||
|
StageID: int
|
||||||
|
StageType: str
|
||||||
|
MonsterList: list
|
||||||
|
|
||||||
|
def get_index(self) -> str:
|
||||||
|
return str(self.StageID)
|
||||||
|
|
||||||
|
|
||||||
12
game_server/resource/configdb/sub_type/anchor_info.py
Normal file
12
game_server/resource/configdb/sub_type/anchor_info.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import dataclasses
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class AnchorInfo:
|
||||||
|
ID: int
|
||||||
|
Name : str
|
||||||
|
PosX : float
|
||||||
|
PosY : float
|
||||||
|
PosZ : float
|
||||||
|
RotX : float
|
||||||
|
RotY : float
|
||||||
|
RotZ : float
|
||||||
35
game_server/resource/configdb/sub_type/floor_info.py
Normal file
35
game_server/resource/configdb/sub_type/floor_info.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import dataclasses
|
||||||
|
from typing import List, Optional, Dict
|
||||||
|
from game_server.resource.configdb.sub_type.group_info import GroupInfo
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class GroupInstance:
|
||||||
|
GroupPath: str
|
||||||
|
ID: int
|
||||||
|
IsDelete: Optional[bool] = False
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class FloorInfo:
|
||||||
|
FloorID: int
|
||||||
|
StartGroupIndex: int
|
||||||
|
StartAnchorID: int
|
||||||
|
GroupInstanceList: List[GroupInstance]
|
||||||
|
groups: Dict[int, GroupInfo] = dataclasses.field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_floor_info(raw_data: dict) -> FloorInfo:
|
||||||
|
group_instances = [
|
||||||
|
GroupInstance(
|
||||||
|
GroupPath=group.get("GroupPath", ""),
|
||||||
|
ID=group.get("ID", 0),
|
||||||
|
IsDelete=group.get("IsDelete", False),
|
||||||
|
)
|
||||||
|
for group in raw_data.get("GroupInstanceList", [])
|
||||||
|
]
|
||||||
|
return FloorInfo(
|
||||||
|
FloorID=raw_data.get("FloorID", 0),
|
||||||
|
StartGroupIndex=raw_data.get("StartGroupIndex", 0),
|
||||||
|
StartAnchorID=raw_data.get("StartAnchorID", 0),
|
||||||
|
GroupInstanceList=group_instances,
|
||||||
|
groups={},
|
||||||
|
)
|
||||||
89
game_server/resource/configdb/sub_type/group_info.py
Normal file
89
game_server/resource/configdb/sub_type/group_info.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import dataclasses
|
||||||
|
from typing import List, Optional,Type, TypeVar
|
||||||
|
from enum import Enum
|
||||||
|
from game_server.resource.configdb.sub_type.npc_info import NpcInfo
|
||||||
|
from game_server.resource.configdb.sub_type.anchor_info import AnchorInfo
|
||||||
|
from game_server.resource.configdb.sub_type.monster_info import MonsterInfo
|
||||||
|
from game_server.resource.configdb.sub_type.prop_info import PropInfo,PropState
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
class GroupLoadSide(Enum):
|
||||||
|
CLIENT = "Client"
|
||||||
|
SERVER = "Server"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_value(cls, value):
|
||||||
|
if value in ("Server"):
|
||||||
|
return cls.SERVER
|
||||||
|
elif value in ("Client"):
|
||||||
|
return cls.CLIENT
|
||||||
|
raise ValueError(f"{value!r} is not a valid GroupLoadSide")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class GroupInfo:
|
||||||
|
NPCList: List[NpcInfo] = dataclasses.field(default_factory=list)
|
||||||
|
id: Optional[int] = dataclasses.field(default_factory=int)
|
||||||
|
OwnerMainMissionID: Optional[int] = dataclasses.field(default_factory=int)
|
||||||
|
LoadOnInitial: Optional[bool] = dataclasses.field(default_factory=bool)
|
||||||
|
AnchorList: Optional[List[AnchorInfo]] = dataclasses.field(default_factory=list)
|
||||||
|
MonsterList: Optional[List[MonsterInfo]] = dataclasses.field(default_factory=list)
|
||||||
|
PropList: Optional[List[PropInfo]] = dataclasses.field(default_factory=list)
|
||||||
|
LoadSide: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dataclass(cls: Type[T], raw_data: dict) -> T:
|
||||||
|
"""
|
||||||
|
Parse a dictionary into a dataclass, filtering out unknown fields
|
||||||
|
and filling in missing fields with default values.
|
||||||
|
:param cls: The dataclass type to instantiate.
|
||||||
|
:param raw_data: The raw dictionary data.
|
||||||
|
:return: An instance of the dataclass.
|
||||||
|
"""
|
||||||
|
cls_fields = {f.name: f for f in dataclasses.fields(cls)}
|
||||||
|
filtered_data = {}
|
||||||
|
|
||||||
|
for field_name, field_def in cls_fields.items():
|
||||||
|
if field_name in raw_data:
|
||||||
|
filtered_data[field_name] = raw_data[field_name]
|
||||||
|
elif field_def.default is not dataclasses.MISSING:
|
||||||
|
filtered_data[field_name] = field_def.default
|
||||||
|
elif field_def.default_factory is not dataclasses.MISSING:
|
||||||
|
filtered_data[field_name] = field_def.default_factory()
|
||||||
|
else:
|
||||||
|
if field_def.type == float:
|
||||||
|
filtered_data[field_name] = 0.0
|
||||||
|
elif field_def.type == int:
|
||||||
|
filtered_data[field_name] = 0
|
||||||
|
elif field_def.type == str:
|
||||||
|
filtered_data[field_name] = ""
|
||||||
|
elif field_def.type == list:
|
||||||
|
filtered_data[field_name] = []
|
||||||
|
elif field_def.type == dict:
|
||||||
|
filtered_data[field_name] = {}
|
||||||
|
else:
|
||||||
|
filtered_data[field_name] = None
|
||||||
|
|
||||||
|
return cls(**filtered_data)
|
||||||
|
|
||||||
|
def parse_group_info(raw_data: dict) -> GroupInfo:
|
||||||
|
def parse_state(state):
|
||||||
|
return PropState[state].value
|
||||||
|
|
||||||
|
return GroupInfo(
|
||||||
|
id=raw_data.get("id"),
|
||||||
|
OwnerMainMissionID=raw_data.get("OwnerMainMissionID"),
|
||||||
|
LoadOnInitial=raw_data.get("LoadOnInitial", False),
|
||||||
|
NPCList=[parse_dataclass(NpcInfo, npc) for npc in (raw_data.get("NPCList") or [])],
|
||||||
|
AnchorList=[parse_dataclass(AnchorInfo, anchor) for anchor in (raw_data.get("AnchorList") or [])],
|
||||||
|
MonsterList=[parse_dataclass(MonsterInfo, monster) for monster in (raw_data.get("MonsterList") or [])],
|
||||||
|
PropList=[
|
||||||
|
parse_dataclass(
|
||||||
|
PropInfo,
|
||||||
|
{k: (parse_state(v) if k == "State" else v) for k, v in prop.items()}
|
||||||
|
)
|
||||||
|
for prop in (raw_data.get("PropList") or [])
|
||||||
|
],
|
||||||
|
LoadSide=raw_data.get("LoadSide") if raw_data.get("LoadSide") else None,
|
||||||
|
)
|
||||||
15
game_server/resource/configdb/sub_type/monster_info.py
Normal file
15
game_server/resource/configdb/sub_type/monster_info.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import dataclasses
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class MonsterInfo:
|
||||||
|
ID : int
|
||||||
|
NPCMonsterID : int
|
||||||
|
EventID : int
|
||||||
|
FarmElementID : int
|
||||||
|
IsClientOnly : bool
|
||||||
|
PosX : float
|
||||||
|
PosY : float
|
||||||
|
PosZ : float
|
||||||
|
RotX : float
|
||||||
|
RotY : float
|
||||||
|
RotZ : float
|
||||||
13
game_server/resource/configdb/sub_type/npc_info.py
Normal file
13
game_server/resource/configdb/sub_type/npc_info.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import dataclasses
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class NpcInfo:
|
||||||
|
ID : int
|
||||||
|
NPCID : int
|
||||||
|
IsClientOnly: bool
|
||||||
|
PosX : float
|
||||||
|
PosY : float
|
||||||
|
PosZ : float
|
||||||
|
RotX : float
|
||||||
|
RotY : float
|
||||||
|
RotZ : float
|
||||||
65
game_server/resource/configdb/sub_type/prop_info.py
Normal file
65
game_server/resource/configdb/sub_type/prop_info.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
import dataclasses
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class PropState(Enum):
|
||||||
|
Closed = 0
|
||||||
|
Open = 1
|
||||||
|
Locked = 2
|
||||||
|
BridgeState1 = 3
|
||||||
|
BridgeState2 = 4
|
||||||
|
BridgeState3 = 5
|
||||||
|
BridgeState4 = 6
|
||||||
|
CheckPointDisable = 7
|
||||||
|
CheckPointEnable = 8
|
||||||
|
TriggerDisable = 9
|
||||||
|
TriggerEnable = 10
|
||||||
|
ChestLocked = 11
|
||||||
|
ChestClosed = 12
|
||||||
|
ChestUsed = 13
|
||||||
|
Elevator1 = 14
|
||||||
|
Elevator2 = 15
|
||||||
|
Elevator3 = 16
|
||||||
|
WaitActive = 17
|
||||||
|
EventClose = 18
|
||||||
|
EventOpen = 19
|
||||||
|
Hidden = 20
|
||||||
|
TeleportGate0 = 21
|
||||||
|
TeleportGate1 = 22
|
||||||
|
TeleportGate2 = 23
|
||||||
|
TeleportGate3 = 24
|
||||||
|
Destructed = 25
|
||||||
|
CustomState01 = 101
|
||||||
|
CustomState02 = 102
|
||||||
|
CustomState03 = 103
|
||||||
|
CustomState04 = 104
|
||||||
|
CustomState05 = 105
|
||||||
|
CustomState06 = 106
|
||||||
|
CustomState07 = 107
|
||||||
|
CustomState08 = 108
|
||||||
|
CustomState09 = 109
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ValueSource:
|
||||||
|
Values: Optional[List] = dataclasses.field(default_factory=list)
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class PropInfo:
|
||||||
|
ID : int
|
||||||
|
MappingInfoID : int
|
||||||
|
AnchorGroupID : int
|
||||||
|
AnchorID : int
|
||||||
|
PropID : int
|
||||||
|
EventID : int
|
||||||
|
CocoonID : int
|
||||||
|
FarmElementID : int
|
||||||
|
IsClientOnly : int
|
||||||
|
State: PropState
|
||||||
|
PosX : float
|
||||||
|
PosY : float
|
||||||
|
PosZ : float
|
||||||
|
RotX : float
|
||||||
|
RotY : float
|
||||||
|
RotZ : float
|
||||||
|
IsDelete: bool
|
||||||
|
Name : str
|
||||||
17
game_server/resource/decorators.py
Normal file
17
game_server/resource/decorators.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from typing import Type, Dict
|
||||||
|
|
||||||
|
resource_registry: Dict[Type, Dict[str, any]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class LoadPriority:
|
||||||
|
HIGH = 1
|
||||||
|
NORMAL = 2
|
||||||
|
LOW = 3
|
||||||
|
|
||||||
|
|
||||||
|
def GameResource(path: str, load_priority=LoadPriority.NORMAL):
|
||||||
|
def decorator(cls):
|
||||||
|
resource_registry[cls] = {"path": path, "load_priority": load_priority}
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return decorator
|
||||||
3
gameserver
Normal file
3
gameserver
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from game_server.main import fn_main
|
||||||
|
|
||||||
|
fn_main()
|
||||||
1896
rail_proto/cmd.py
Normal file
1896
rail_proto/cmd.py
Normal file
File diff suppressed because it is too large
Load Diff
23969
rail_proto/lib.py
Normal file
23969
rail_proto/lib.py
Normal file
File diff suppressed because it is too large
Load Diff
44
sdk_server/__init__.py
Normal file
44
sdk_server/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import logging
|
||||||
|
from flask import Flask
|
||||||
|
from sdk_server.controllers.config.agreement_controller import agreement_blueprint
|
||||||
|
from sdk_server.controllers.config.alert_ann_controller import alert_ann_blueprint
|
||||||
|
from sdk_server.controllers.config.alert_pict_controller import alert_pic_blueprint
|
||||||
|
from sdk_server.controllers.config.batch_upload_controller import batch_upload_blueprint
|
||||||
|
from sdk_server.controllers.config.combo_config_controller import combo_config_blueprint
|
||||||
|
from sdk_server.controllers.config.combo_controller import combo_blueprint
|
||||||
|
from sdk_server.controllers.config.compare_protocol_ver_controller import compare_protocol_ver_blueprint
|
||||||
|
from sdk_server.controllers.config.data_upload_controller import data_upload_blueprint
|
||||||
|
from sdk_server.controllers.config.risky_check_controller import risky_check_blueprint
|
||||||
|
from sdk_server.controllers.login.login_controller import login_blueprint
|
||||||
|
from sdk_server.controllers.login.login_v2_controller import login_v2_blueprint
|
||||||
|
from sdk_server.controllers.login.query_dispatch_controller import query_dispatch_blueprint
|
||||||
|
from sdk_server.controllers.login.query_gateway_controller import query_gateway_blueprint
|
||||||
|
from sdk_server.controllers.login.token_login_controller import token_login_blueprint
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
log = logging.getLogger('werkzeug')
|
||||||
|
log.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
# CONFIG
|
||||||
|
app.register_blueprint(agreement_blueprint)
|
||||||
|
app.register_blueprint(alert_ann_blueprint)
|
||||||
|
app.register_blueprint(alert_pic_blueprint)
|
||||||
|
app.register_blueprint(batch_upload_blueprint)
|
||||||
|
app.register_blueprint(combo_config_blueprint)
|
||||||
|
app.register_blueprint(combo_blueprint)
|
||||||
|
app.register_blueprint(compare_protocol_ver_blueprint)
|
||||||
|
app.register_blueprint(data_upload_blueprint)
|
||||||
|
app.register_blueprint(risky_check_blueprint)
|
||||||
|
|
||||||
|
# LOGIN
|
||||||
|
app.register_blueprint(login_blueprint)
|
||||||
|
app.register_blueprint(login_v2_blueprint)
|
||||||
|
app.register_blueprint(query_dispatch_blueprint)
|
||||||
|
app.register_blueprint(query_gateway_blueprint)
|
||||||
|
app.register_blueprint(token_login_blueprint)
|
||||||
|
|
||||||
|
def run_http_server(host, port):
|
||||||
|
app.run(host=host, port=port, debug=False)
|
||||||
|
|
||||||
16
sdk_server/controllers/config/agreement_controller.py
Normal file
16
sdk_server/controllers/config/agreement_controller.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.agreement_data import AgreementInfoRsp
|
||||||
|
|
||||||
|
agreement_blueprint = Blueprint('agreement', __name__)
|
||||||
|
|
||||||
|
@agreement_blueprint.route('/hkrpg_cn/mdk/agreement/api/getAgreementInfos', methods=['GET'])
|
||||||
|
@agreement_blueprint.route('/hkrpg_global/mdk/agreement/api/getAgreementInfos', methods=['GET'])
|
||||||
|
def agreement():
|
||||||
|
response_data = AgreementInfoRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="OK",
|
||||||
|
data=AgreementInfoRsp.Data(
|
||||||
|
marketing_agreements=[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
19
sdk_server/controllers/config/alert_ann_controller.py
Normal file
19
sdk_server/controllers/config/alert_ann_controller.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.alert_ann_data import AlertAnnRsp
|
||||||
|
|
||||||
|
alert_ann_blueprint = Blueprint('alert_ann', __name__)
|
||||||
|
|
||||||
|
@alert_ann_blueprint.route('/common/hkrpg_cn/announcement/api/getAlertAnn', methods=['GET'])
|
||||||
|
@alert_ann_blueprint.route('/common/hkrpg_global/announcement/api/getAlertAnn', methods=['GET'])
|
||||||
|
def alert_ann():
|
||||||
|
response_data = AlertAnnRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="OK",
|
||||||
|
data=AlertAnnRsp.Data(
|
||||||
|
alert=False,
|
||||||
|
alert_id=0,
|
||||||
|
remind=True,
|
||||||
|
extra_remind=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
17
sdk_server/controllers/config/alert_pict_controller.py
Normal file
17
sdk_server/controllers/config/alert_pict_controller.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.alert_pic_data import AlertPicRsp
|
||||||
|
|
||||||
|
alert_pic_blueprint = Blueprint('alert_pic', __name__)
|
||||||
|
|
||||||
|
@alert_pic_blueprint.route('/common/hkrpg_cn/announcement/api/getAlertPic', methods=['GET'])
|
||||||
|
@alert_pic_blueprint.route('/common/hkrpg_global/announcement/api/getAlertPic', methods=['GET'])
|
||||||
|
def alert_pic():
|
||||||
|
response_data = AlertPicRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="OK",
|
||||||
|
data=AlertPicRsp.Data(
|
||||||
|
total=0,
|
||||||
|
list=[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
13
sdk_server/controllers/config/batch_upload_controller.py
Normal file
13
sdk_server/controllers/config/batch_upload_controller.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.batch_upload_data import BatchUploadRsp
|
||||||
|
|
||||||
|
batch_upload_blueprint = Blueprint('batch_upload', __name__)
|
||||||
|
|
||||||
|
@batch_upload_blueprint.route('/common/h5log/log/batch', methods=['POST'])
|
||||||
|
def batch_upload():
|
||||||
|
response_data = BatchUploadRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="success",
|
||||||
|
data=[]
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
35
sdk_server/controllers/config/combo_config_controller.py
Normal file
35
sdk_server/controllers/config/combo_config_controller.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.combo_config_data import ComboConfigRsp,QrEnabledApps,QrAppIcons
|
||||||
|
|
||||||
|
combo_config_blueprint = Blueprint('combo_config', __name__)
|
||||||
|
|
||||||
|
@combo_config_blueprint.route('/hkrpg_cn/combo/granter/api/getConfig', methods=['GET'])
|
||||||
|
@combo_config_blueprint.route('/hkrpg_global/combo/granter/api/getConfig', methods=['GET'])
|
||||||
|
def combo_config():
|
||||||
|
response_data = ComboConfigRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="OK",
|
||||||
|
data=ComboConfigRsp.Data(
|
||||||
|
protocol = True,
|
||||||
|
qr_enabled = False,
|
||||||
|
log_level = "INFO",
|
||||||
|
announce_url = "",
|
||||||
|
push_alias_type = 0,
|
||||||
|
disable_ysdk_guard = True,
|
||||||
|
enable_announce_pic_popup = False,
|
||||||
|
app_name = "崩坏RPG",
|
||||||
|
qr_enabled_apps=QrEnabledApps(
|
||||||
|
bbs=False,
|
||||||
|
cloud=False
|
||||||
|
),
|
||||||
|
qr_app_icons=QrAppIcons(
|
||||||
|
app="",
|
||||||
|
bbs="",
|
||||||
|
cloud=""
|
||||||
|
),
|
||||||
|
qr_cloud_display_name="",
|
||||||
|
enable_user_center=False,
|
||||||
|
functional_switch_configs=[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
51
sdk_server/controllers/config/combo_controller.py
Normal file
51
sdk_server/controllers/config/combo_controller.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.combo_data import (
|
||||||
|
ComboRsp,
|
||||||
|
KibanaPc,
|
||||||
|
Report,
|
||||||
|
Telemetry,
|
||||||
|
LogFilter,
|
||||||
|
RenderMethod,
|
||||||
|
Function
|
||||||
|
)
|
||||||
|
|
||||||
|
combo_blueprint = Blueprint('combo', __name__)
|
||||||
|
|
||||||
|
@combo_blueprint.route('/combo/box/api/config/sdk/combo', methods=['GET'])
|
||||||
|
def combo():
|
||||||
|
response_data = ComboRsp(
|
||||||
|
vals=ComboRsp.Values(
|
||||||
|
kibana_pc_config=KibanaPc(
|
||||||
|
enable=1,
|
||||||
|
level="Info",
|
||||||
|
modules=["download"]
|
||||||
|
),
|
||||||
|
network_report_config=Report(
|
||||||
|
enable=1,
|
||||||
|
status_codes=[206],
|
||||||
|
url_paths=["dataUpload", "red_dot"]
|
||||||
|
),
|
||||||
|
modify_real_name_other_verify=True,
|
||||||
|
telemetry_config=Telemetry(
|
||||||
|
dataupload_enable=1
|
||||||
|
),
|
||||||
|
enable_web_dpi=True,
|
||||||
|
h5log_filter_config=LogFilter(
|
||||||
|
function=Function(
|
||||||
|
event_name=[
|
||||||
|
"info_get_cps",
|
||||||
|
"notice_close_notice",
|
||||||
|
"info_get_uapc",
|
||||||
|
"report_set_info",
|
||||||
|
"info_get_channel_id",
|
||||||
|
"info_get_sub_channel_id"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
webview_rendermethod_config=RenderMethod(
|
||||||
|
use_legacy=True
|
||||||
|
),
|
||||||
|
list_price_tierv2_enable=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.compare_protocol_ver_data import CompareProtocolVerRsp
|
||||||
|
|
||||||
|
compare_protocol_ver_blueprint = Blueprint('compare_protocol_ver', __name__)
|
||||||
|
|
||||||
|
@compare_protocol_ver_blueprint.route('/hkrpg_cn/combo/granter/api/compareProtocolVersion', methods=['POST'])
|
||||||
|
@compare_protocol_ver_blueprint.route('/hkrpg_global/combo/granter/api/compareProtocolVersion', methods=['POST'])
|
||||||
|
def compare_protocol_ver():
|
||||||
|
response_data = CompareProtocolVerRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="OK",
|
||||||
|
data=CompareProtocolVerRsp.Data(
|
||||||
|
modified=False,
|
||||||
|
protocol=[]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
12
sdk_server/controllers/config/data_upload_controller.py
Normal file
12
sdk_server/controllers/config/data_upload_controller.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.log_upload_data import LogUploadRsp
|
||||||
|
|
||||||
|
data_upload_blueprint = Blueprint('data_upload', __name__)
|
||||||
|
|
||||||
|
@data_upload_blueprint.route('/sdk/dataUpload', methods=['POST'])
|
||||||
|
@data_upload_blueprint.route('/loginsdk/dataUpload', methods=['POST'])
|
||||||
|
@data_upload_blueprint.route('/crashdump/dataUpload', methods=['POST'])
|
||||||
|
@data_upload_blueprint.route('/apm/dataUpload', methods=['POST'])
|
||||||
|
def data_upload():
|
||||||
|
response_data = LogUploadRsp(code=0)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
17
sdk_server/controllers/config/risky_check_controller.py
Normal file
17
sdk_server/controllers/config/risky_check_controller.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from sdk_server.models.config.risky_check_data import RiskyCheckRsp
|
||||||
|
|
||||||
|
risky_check_blueprint = Blueprint('risky_check', __name__)
|
||||||
|
|
||||||
|
@risky_check_blueprint.route('/account/risky/api/check', methods=['POST'])
|
||||||
|
def risky_check():
|
||||||
|
response_data = RiskyCheckRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="OK",
|
||||||
|
data=RiskyCheckRsp.Data(
|
||||||
|
id="none",
|
||||||
|
action="ACTION_NONE",
|
||||||
|
geetest={}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(response_data.model_dump())
|
||||||
42
sdk_server/controllers/login/login_controller.py
Normal file
42
sdk_server/controllers/login/login_controller.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from database.account.account_data import create_new_account
|
||||||
|
from sdk_server.models.login.login_data import LoginReq,LoginRsp,Account
|
||||||
|
from database.account.account_data import find_account_by_name
|
||||||
|
|
||||||
|
login_blueprint = Blueprint('login', __name__)
|
||||||
|
|
||||||
|
@login_blueprint.route('/hkrpg_cn/mdk/shield/api/login', methods=['POST'])
|
||||||
|
@login_blueprint.route('/hkrpg_global/mdk/shield/api/login', methods=['POST'])
|
||||||
|
@login_blueprint.route('/account/ma-cn-passport/app/loginByPassword', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
body=request.get_json()
|
||||||
|
try:
|
||||||
|
login_req = LoginReq(**body)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "Invalid input", "details": str(e)}), 400
|
||||||
|
account_data=find_account_by_name(login_req.account)
|
||||||
|
if not account_data:
|
||||||
|
create_new_account(login_req.account)
|
||||||
|
account_data=find_account_by_name(login_req.account)
|
||||||
|
|
||||||
|
rsp=LoginRsp(
|
||||||
|
retcode=0,
|
||||||
|
message="success",
|
||||||
|
data=LoginRsp.Data(
|
||||||
|
account=Account(
|
||||||
|
uid=str(account_data.id),
|
||||||
|
name=account_data.username + "@MikuMiku",
|
||||||
|
token=account_data.token,
|
||||||
|
is_email_verify=0,
|
||||||
|
realname="Miku",
|
||||||
|
identity_card="114514",
|
||||||
|
country="OS",
|
||||||
|
area_code="OS",
|
||||||
|
),
|
||||||
|
device_grant_required=False,
|
||||||
|
realname_operation="NONE",
|
||||||
|
realperson_required=False,
|
||||||
|
safe_mobile_required=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return jsonify(rsp.model_dump())
|
||||||
36
sdk_server/controllers/login/login_v2_controller.py
Normal file
36
sdk_server/controllers/login/login_v2_controller.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import json
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from utils.crypto import generate_combo_token
|
||||||
|
from sdk_server.models.login.login_v2_data import LoginV2Req,LoginV2Rsp
|
||||||
|
from database.account.account_data import find_account_by_uid
|
||||||
|
|
||||||
|
login_v2_blueprint = Blueprint('login_v2', __name__)
|
||||||
|
|
||||||
|
@login_v2_blueprint.route('/hkrpg_cn/combo/granter/login/v2/login', methods=['POST'])
|
||||||
|
@login_v2_blueprint.route('/hkrpg_global/combo/granter/login/v2/login', methods=['POST'])
|
||||||
|
def login_v2():
|
||||||
|
res=LoginV2Rsp()
|
||||||
|
body=request.json
|
||||||
|
login_v2_req=LoginV2Req(**body)
|
||||||
|
data_dict = json.loads(login_v2_req.data)
|
||||||
|
token_data = LoginV2Req.Data(**data_dict)
|
||||||
|
if not token_data:
|
||||||
|
res.retcode=0
|
||||||
|
res.message="Invalid login data"
|
||||||
|
return jsonify(res.model_dump())
|
||||||
|
|
||||||
|
account_data=find_account_by_uid(int(token_data.uid))
|
||||||
|
if not account_data:
|
||||||
|
res.retcode=-201
|
||||||
|
res.message="Game account cache information error"
|
||||||
|
return jsonify(res.model_dump())
|
||||||
|
|
||||||
|
res.message="OK"
|
||||||
|
res.data=LoginV2Rsp.Data(
|
||||||
|
account_type=1,
|
||||||
|
open_id=str(account_data.id),
|
||||||
|
combo_token=generate_combo_token(str(account_data.id)),
|
||||||
|
data="{\"guest\":false}"
|
||||||
|
)
|
||||||
|
return jsonify(res.model_dump())
|
||||||
|
|
||||||
20
sdk_server/controllers/login/query_dispatch_controller.py
Normal file
20
sdk_server/controllers/login/query_dispatch_controller.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import base64
|
||||||
|
from flask import Blueprint
|
||||||
|
from rail_proto.lib import Dispatch,RegionInfo
|
||||||
|
|
||||||
|
query_dispatch_blueprint = Blueprint('query_dispatch', __name__)
|
||||||
|
|
||||||
|
@query_dispatch_blueprint.route("/query_dispatch", methods=["GET"])
|
||||||
|
def query_dispatch():
|
||||||
|
rsp = Dispatch(
|
||||||
|
retcode=0,
|
||||||
|
region_list=[
|
||||||
|
RegionInfo(
|
||||||
|
name="NeonSR",
|
||||||
|
title="NeonSR",
|
||||||
|
env_type="21",
|
||||||
|
dispatch_url="http://127.0.0.1:21000/query_gateway",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return base64.b64encode(rsp.SerializeToString()).decode()
|
||||||
45
sdk_server/controllers/login/query_gateway_controller.py
Normal file
45
sdk_server/controllers/login/query_gateway_controller.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from flask import Blueprint,request
|
||||||
|
from rail_proto.lib import GateServer
|
||||||
|
|
||||||
|
query_gateway_blueprint = Blueprint('query_gateway', __name__)
|
||||||
|
|
||||||
|
@query_gateway_blueprint.route("/query_gateway", methods=["GET"])
|
||||||
|
def query_gateway():
|
||||||
|
version = request.args.get('version')
|
||||||
|
with open('version.json', 'r') as f:
|
||||||
|
reader = json.load(f)
|
||||||
|
config = reader.get(version)
|
||||||
|
if config:
|
||||||
|
rsp = GateServer(
|
||||||
|
retcode=0,
|
||||||
|
ip="127.0.0.1",
|
||||||
|
port=23301,
|
||||||
|
asset_bundle_url="",
|
||||||
|
lua_url="",
|
||||||
|
ex_resource_url="",
|
||||||
|
mdk_res_version="",
|
||||||
|
enable_version_update=True,
|
||||||
|
enable_design_data_version_update=True,
|
||||||
|
enable_save_replay_file=True,
|
||||||
|
enable_upload_battle_log=True,
|
||||||
|
enable_watermark=True,
|
||||||
|
event_tracking_open=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rsp = GateServer(
|
||||||
|
retcode=0,
|
||||||
|
ip="127.0.0.1",
|
||||||
|
port=23301,
|
||||||
|
asset_bundle_url="",
|
||||||
|
lua_url="",
|
||||||
|
ex_resource_url="",
|
||||||
|
enable_version_update=True,
|
||||||
|
enable_design_data_version_update=True,
|
||||||
|
enable_save_replay_file=True,
|
||||||
|
enable_upload_battle_log=True,
|
||||||
|
enable_watermark=True,
|
||||||
|
event_tracking_open=True,
|
||||||
|
)
|
||||||
|
return base64.b64encode(rsp.SerializeToString()).decode()
|
||||||
40
sdk_server/controllers/login/token_login_controller.py
Normal file
40
sdk_server/controllers/login/token_login_controller.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from sdk_server.models.login.token_login_data import TokenLoginReq
|
||||||
|
from sdk_server.models.login.login_data import LoginRsp,Account
|
||||||
|
|
||||||
|
from database.account.account_data import find_account_by_uid
|
||||||
|
|
||||||
|
token_login_blueprint = Blueprint('token_login', __name__)
|
||||||
|
|
||||||
|
@token_login_blueprint.route('/hkrpg_cn/mdk/shield/api/verify', methods=['POST'])
|
||||||
|
@token_login_blueprint.route('/hkrpg_global/mdk/shield/api/verify', methods=['POST'])
|
||||||
|
@token_login_blueprint.route('/account/ma-cn-session/app/verify', methods=['POST'])
|
||||||
|
def token_login():
|
||||||
|
res=LoginRsp()
|
||||||
|
body=request.json
|
||||||
|
req=TokenLoginReq(**body)
|
||||||
|
account_data=find_account_by_uid(int(req.uid))
|
||||||
|
if not account_data or account_data.token != req.token:
|
||||||
|
res.retcode=0
|
||||||
|
res.message="Game account cache information error"
|
||||||
|
else:
|
||||||
|
res.retcode=0
|
||||||
|
res.message="OK"
|
||||||
|
res.data=LoginRsp.Data(
|
||||||
|
account=Account(
|
||||||
|
uid=str(account_data.id),
|
||||||
|
name=account_data.username + "@MikuMiku",
|
||||||
|
token=account_data.token,
|
||||||
|
is_email_verify=0,
|
||||||
|
realname="Miku",
|
||||||
|
identity_card="114514",
|
||||||
|
country="OS",
|
||||||
|
area_code="OS",
|
||||||
|
),
|
||||||
|
device_grant_required=False,
|
||||||
|
realname_operation="NONE",
|
||||||
|
realperson_required=False,
|
||||||
|
safe_mobile_required=False
|
||||||
|
)
|
||||||
|
return jsonify(res.model_dump())
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user