This commit is contained in:
Naruse
2025-04-15 19:36:05 +08:00
parent dd51fb491d
commit ec8972d5d6
121 changed files with 30598 additions and 0 deletions

View 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()

View 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

View 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}")

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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

View 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={},
)

View 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,
)

View 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

View 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

View 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

View 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