refactor(debug-mod): 使用原生 System 替换 QuMod

删除未使用的大型 QuMod 加载框架,改为直接注册客户端和服务端 System,并显式管理按键监听、输出包装与双端销毁。

IPC 网络线程仅负责收包,所有 ModAPI 回调由游戏线程 Update 派发,同时补充断线重连、分帧读取和幂等关闭。
This commit is contained in:
2026-07-21 20:52:10 +08:00
parent 3afc122404
commit 94b1b9a1f5
15 changed files with 386 additions and 1057 deletions

View File

@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
from . import IPCSystem, Lifecycle
from .Config import DEBUG_CONFIG
from .Game import (
INIT_RELOAD_TIME,
RELOAD_ADDON,
RELOAD_MOD,
RELOAD_SHADERS,
RELOAD_WORLD,
)
ClientSystem = clientApi.GetClientSystemCls()
ENGINE_NAMESPACE = clientApi.GetEngineNamespace()
ENGINE_SYSTEM_NAME = clientApi.GetEngineSystemName()
class DebugClientSystem(ClientSystem):
def __init__(self, namespace, system_name):
ClientSystem.__init__(self, namespace, system_name)
self._initialized = False
self._listening = False
def Update(self):
if not self._initialized:
Lifecycle.activate(self)
IPCSystem.ON_CLIENT_INIT()
self.ListenForEvent(
ENGINE_NAMESPACE,
ENGINE_SYSTEM_NAME,
"OnKeyPressInGame",
self,
self.on_key_press_in_game,
)
self._listening = True
self._initialized = True
INIT_RELOAD_TIME()
IPCSystem.UPDATE_CLIENT()
return ClientSystem.Update(self)
def Destroy(self):
try:
if self._listening:
self.UnListenForEvent(
ENGINE_NAMESPACE,
ENGINE_SYSTEM_NAME,
"OnKeyPressInGame",
self,
self.on_key_press_in_game,
)
self._listening = False
finally:
try:
IPCSystem.ON_CLIENT_EXIT()
finally:
self._initialized = False
Lifecycle.deactivate(self)
def on_key_press_in_game(self, args):
if args["isDown"] != "0":
return
if args["screenName"] != "hud_screen" and not DEBUG_CONFIG.get(
"reload_key_global", False
):
return
key = args["key"]
if key == str(DEBUG_CONFIG.get("reload_key", "82")):
RELOAD_MOD()
elif key == str(DEBUG_CONFIG.get("reload_world_key", "")):
RELOAD_WORLD()
elif key == str(DEBUG_CONFIG.get("reload_addon_key", "")):
RELOAD_ADDON()
elif key == str(DEBUG_CONFIG.get("reload_shaders_key", "")):
RELOAD_SHADERS()

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
from . import IPCSystem, Lifecycle
ServerSystem = serverApi.GetServerSystemCls()
class DebugServerSystem(ServerSystem):
def __init__(self, namespace, system_name):
ServerSystem.__init__(self, namespace, system_name)
self._initialized = False
def Update(self):
if not self._initialized:
Lifecycle.activate(self)
IPCSystem.ON_SERVER_INIT()
self._initialized = True
IPCSystem.UPDATE_SERVER()
return ServerSystem.Update(self)
def Destroy(self):
try:
IPCSystem.ON_SERVER_EXIT()
finally:
self._initialized = False
Lifecycle.deactivate(self)

View File

@@ -4,6 +4,7 @@ import mod.client.extraClientApi as clientApi
from .Config import GET_DEBUG_IPC_PORT
import socket
import threading
import time
import json
def U16_BE(b):
@@ -24,74 +25,162 @@ class IPCSystem:
self.port = port
self.sock = None
self.mLock = threading.Lock()
self.handers = {}
self.handlers = {}
self.pendingLock = threading.Lock()
self.pendingClient = []
self.pendingServer = []
self.running = False
self.generation = 0
self.thread = None
self.nextStartTime = 0.0
def registerHandler(self, typeID, handler):
# type: (int, callable) -> None
self.handers[typeID] = handler
self.handlers[typeID] = handler
def updateHandlers(self, handlers):
# type: (dict[int, callable]) -> None
self.handers.update(handlers)
self.handlers.update(handlers)
def start(self):
if self.sock or not self.port:
now = time.time()
with self.mLock:
if self.running or not self.port or now < self.nextStartTime:
return
threading.Thread(target=self._threadListenLoop).start()
self.running = True
self.nextStartTime = now + 1.0
self.generation += 1
generation = self.generation
thread = threading.Thread(
target=self._threadListenLoop,
args=(generation,),
)
thread.daemon = True
self.thread = thread
thread.start()
def close(self):
sock = None
with self.mLock:
self.running = False
self.generation += 1
self.nextStartTime = 0.0
sock = self.sock
self.sock = None
thread = self.thread
if sock:
try:
sock.shutdown(socket.SHUT_RDWR)
except socket.error:
pass
try:
sock.close()
except socket.error:
pass
if thread:
thread.join(1.0)
with self.pendingLock:
self.pendingClient = []
self.pendingServer = []
def _threadListenLoop(self):
def updateClient(self):
self.start()
with self.pendingLock:
pending = self.pendingClient
self.pendingClient = []
self._runPending(pending)
def updateServer(self):
with self.pendingLock:
pending = self.pendingServer
self.pendingServer = []
self._runPending(pending)
@staticmethod
def _runPending(pending):
for handler, data in pending:
try:
handler(data)
except Exception:
import traceback
traceback.print_exc()
def _threadListenLoop(self, generation):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
with self.mLock:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock = self.sock
if not self.running or generation != self.generation:
sock.close()
return
self.sock = sock
try:
sock.settimeout(2.0)
sock.connect(("localhost", self.port))
sock.settimeout(0.05)
sock.settimeout(0.2)
print("[IPCSystem] 已连接到调试服务器,端口:" + str(self.port))
# [2B TypeID][4B DataLength][Data]
def _recvAll(sock, length):
# type: (socket.socket, int) -> bytearray
while self._isCurrent(generation, sock):
header = self._recvAll(generation, sock, 6)
typeID = U16_BE(header[0:2])
dataLength = U32_BE(header[2:6])
data = self._recvAll(generation, sock, dataLength)
self._queueHandler(generation, typeID, data)
except EOFError:
pass
except socket.error:
if self._isCurrent(generation, sock):
import traceback
traceback.print_exc()
except Exception:
import traceback
traceback.print_exc()
finally:
try:
sock.close()
except socket.error:
pass
with self.mLock:
if self.sock is sock:
self.sock = None
if self.thread is threading.currentThread():
self.thread = None
if generation == self.generation:
self.running = False
print("[IPCSystem] 连接已关闭")
def _isCurrent(self, generation, sock):
with self.mLock:
return (
self.running
and generation == self.generation
and self.sock is sock
)
def _queueHandler(self, generation, typeID, data):
handler = self.handlers.get(typeID)
if handler is None:
print("[IPCSystem] 未知的TypeID数据包" + str(typeID))
return
with self.mLock:
if not self.running or generation != self.generation:
return
with self.pendingLock:
if typeID == 4:
self.pendingServer.append((handler, data))
else:
self.pendingClient.append((handler, data))
def _recvAll(self, generation, sock, length):
buf = bytearray()
while len(buf) < length:
try:
more = sock.recv(length - len(buf))
except socket.timeout:
if not self._isCurrent(generation, sock):
raise EOFError("IPC connection stopped")
continue
if not more:
raise EOFError("Socket closed before receiving all data")
buf.extend(more)
return buf
while 1:
try:
header = _recvAll(sock, 6)
typeID = U16_BE(header[0:2])
dataLength = U32_BE(header[2:6])
data = _recvAll(sock, dataLength)
except socket.timeout:
continue
except EOFError:
break
except socket.error:
break
except Exception:
import traceback
traceback.print_exc()
break
if typeID in self.handers:
try:
self.handers[typeID](data)
except Exception:
import traceback
traceback.print_exc()
else:
print("[IPCSystem] 未知的TypeID数据包" + str(typeID))
with self.mLock:
self.sock = None
print("[IPCSystem] 连接已关闭")
_CL_GAME_COMP = None
_SR_GAME_COMP = None
@@ -168,14 +257,25 @@ _IPCSYSTEM.updateHandlers(
}
)
def UPDATE_CLIENT():
_IPCSYSTEM.updateClient()
def UPDATE_SERVER():
_IPCSYSTEM.updateServer()
def ON_CLIENT_INIT():
global _CL_GAME_COMP
_CL_GAME_COMP = clientApi.GetEngineCompFactory().CreateGame(clientApi.GetLevelId())
_IPCSYSTEM.start()
def ON_CLIENT_EXIT():
global _CL_GAME_COMP
_IPCSYSTEM.close()
_CL_GAME_COMP = None
def ON_SERVER_INIT():
global _SR_GAME_COMP
_SR_GAME_COMP = serverApi.GetEngineCompFactory().CreateGame(serverApi.GetLevelId())
def ON_SERVER_EXIT():
global _SR_GAME_COMP
_SR_GAME_COMP = None

View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
import sys
import threading
_OUTPUT_PREFIX = "[Python] "
_ACTIVE_SYSTEMS = set()
_STATE_LOCK = threading.Lock()
_ORIGINAL_STDOUT = None
_ORIGINAL_STDERR = None
class OutputWrapper(object):
_emod_cli_debug_wrapper = True
def __init__(self, stream):
self.stream = stream
self.lock = threading.Lock()
self.buffer = []
def __getattr__(self, name):
return getattr(self.stream, name)
def write(self, data):
with self.lock:
parts = data.splitlines(True)
for part in parts:
if part.endswith("\n"):
if self.buffer:
line = "".join(self.buffer) + part
self.buffer = []
else:
line = part
self.stream.write(_OUTPUT_PREFIX + line)
else:
self.buffer.append(part)
def close(self):
self.flush()
return self.stream.close()
def flush(self):
with self.lock:
if self.buffer:
self.stream.write(_OUTPUT_PREFIX + "".join(self.buffer))
self.buffer = []
self.stream.flush()
def writelines(self, lines):
for line in lines:
self.write(line)
def fileno(self):
return self.stream.fileno()
def _install_output_wrapper_locked():
global _ORIGINAL_STDOUT, _ORIGINAL_STDERR
if not getattr(sys.stdout, "_emod_cli_debug_wrapper", False):
_ORIGINAL_STDOUT = sys.stdout
sys.stdout = OutputWrapper(sys.stdout)
elif _ORIGINAL_STDOUT is None:
_ORIGINAL_STDOUT = getattr(sys.stdout, "stream")
if not getattr(sys.stderr, "_emod_cli_debug_wrapper", False):
_ORIGINAL_STDERR = sys.stderr
sys.stderr = OutputWrapper(sys.stderr)
elif _ORIGINAL_STDERR is None:
_ORIGINAL_STDERR = getattr(sys.stderr, "stream")
def install_output_wrapper():
with _STATE_LOCK:
_install_output_wrapper_locked()
def activate(system):
with _STATE_LOCK:
_install_output_wrapper_locked()
_ACTIVE_SYSTEMS.add(system)
def deactivate(system):
global _ORIGINAL_STDOUT, _ORIGINAL_STDERR
with _STATE_LOCK:
_ACTIVE_SYSTEMS.discard(system)
if _ACTIVE_SYSTEMS:
return
if _ORIGINAL_STDOUT is None or _ORIGINAL_STDERR is None:
return
sys.stdout.flush()
sys.stderr.flush()
sys.stdout = _ORIGINAL_STDOUT
sys.stderr = _ORIGINAL_STDERR
_ORIGINAL_STDOUT = None
_ORIGINAL_STDERR = None

View File

@@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
from .Util import SystemSide
lambda: "By Zero123"
IsServerUser = False
ModDirName = SystemSide.__module__.split(".")[0]
QuModLibsPath = SystemSide.__module__[:SystemSide.__module__.rfind(".")]
class RuntimeService:
_serverSystemList = []
_clientSystemList = []
_serverStarting = False
_clientStarting = False
# LOADER SYSTEM
_serverLoadBefore = [] # type: list[function]
_clientLoadBefore = [] # type: list[function]
_serverLoadFinish = [] # type: list[function]
_clientLoadFinish = [] # type: list[function]
# THREAD ID
_serverThreadID = None
_clientThreadID = None
_envPlayerId = None
def getUnderlineModDirName():
# type: () -> str
""" 获取下划线MOD目录名称 返回结果与preset内置变量__LQuModName__一致 (仅支持ascii字符串) """
newStr = [] # type: list[int]
for i, _charStr in enumerate(ModDirName):
_char = ord(_charStr)
if (_char >= 65 and _char <= 90):
# 大写内容 进行处理
if i > 0:
newStr.append(ord("_"))
newStr.append(_char + (97 - 65))
continue
# 常规小写内容 直接追加
newStr.append(_char)
return "".join((chr(x) for x in newStr))
def GET_THREAD_ID():
""" 获取当前线程ID """
from threading import current_thread
return current_thread().ident
def IS_SERVER_THREAD():
""" 检查是不是服务端线程 """
return RuntimeService._serverThreadID != None and GET_THREAD_ID() == RuntimeService._serverThreadID
def IS_CLIENT_THREAD():
""" 检查是不是客户端线程 """
return RuntimeService._clientThreadID != None and GET_THREAD_ID() == RuntimeService._clientThreadID
def GET_THREAD_TYPE():
""" 获取线程类型 -1.主线程 0.服务端线程 1.客户端线程 """
tid = GET_THREAD_ID()
if tid == RuntimeService._serverThreadID:
return 0
elif tid == RuntimeService._clientThreadID:
return 1
return -1

View File

@@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
Version = "1.4.1" # 版本信息
ApiVersion = 4 # API版本
Author = "Zero123" # 创作者
ContactInformation = "QQ:913702423" # 联系方式
Other = """
# QuModLibs By Zero123(网易:游趣开发组) 别名:一灵 | h2v-wither123... BilBil-UID:456549011
# 开源协议: BSD(3)
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
"""

View File

@@ -1,164 +0,0 @@
# -*- coding: utf-8 -*-
from mod.common.mod import Mod
import mod.server.extraServerApi as serverApi
import mod.client.extraClientApi as clientApi
from .Util import TRY_EXEC_FUN
from . import IN
class _TempData:
""" modMain临时数据储存 包含了初始化注册的一些信息 """
_nativePyServer = []
_nativePyClient = []
_serverInitCall = []
_clientInitCall = []
_threadAnalysis = False
@Mod.Binding(name = "QuMod_"+IN.ModDirName, version = "1.0.0")
class QMain(object):
""" QuMod MAIN入口逻辑 """
def __init__(self):
pass
@Mod.InitServer()
def serverInit(self):
# 服务端初始化
IN.IsServerUser = True
self._regNativePyServer()
self._loadServerInitFuncs()
if _TempData._threadAnalysis:
from threading import current_thread
IN.RuntimeService._serverThreadID = current_thread().ident
if IN.RuntimeService._serverSystemList or IN.RuntimeService._serverLoadBefore:
from .Systems.Loader.Server import LoaderSystem
LoaderSystem.getSystem() # 初始化服务端加载器
@Mod.InitClient()
def clientInit(self):
IN.RuntimeService._envPlayerId = clientApi.GetLocalPlayerId()
self._regNativePyClient()
self._loadClientInitFuncs()
if _TempData._threadAnalysis:
from threading import current_thread
IN.RuntimeService._clientThreadID = current_thread().ident
if IN.RuntimeService._clientSystemList or IN.RuntimeService._clientLoadBefore:
from .Systems.Loader.Client import LoaderSystem
LoaderSystem.getSystem() # 初始化客户端加载器
def _regNativePyClient(self):
""" 加载原版Python客户端系统注册 """
for args in _TempData._nativePyClient:
clientApi.RegisterSystem(*args)
_TempData._nativePyClient = []
def _regNativePyServer(self):
""" 加载原版Python服务端系统注册 """
for args in _TempData._nativePyServer:
serverApi.RegisterSystem(*args)
_TempData._nativePyServer = []
def _loadServerInitFuncs(self):
""" 加载服务端初始化函数 """
for funObj in _TempData._serverInitCall:
TRY_EXEC_FUN(funObj)
_TempData._serverInitCall = []
def _loadClientInitFuncs(self):
""" 加载客户端初始化函数 """
for funObj in _TempData._clientInitCall:
TRY_EXEC_FUN(funObj)
_TempData._clientInitCall = []
@Mod.DestroyServer()
def serverDestroy(self):
pass
@Mod.DestroyClient()
def clientDestroy(self):
pass
class EasyMod:
""" 简易Mod构造器 """
def __init__(self, modDirName=None):
# type: (str | None) -> None
self._modDirName = modDirName if modDirName else IN.ModDirName
""" Mod目录名 """
def regServer(self, relPath="", systemName=None):
# type: (str, str | None) -> EasyMod
""" 注册服务端(相对目录) """
REG_SERVER_MODULE("{}.{}".format(self._modDirName, relPath), systemName)
return self
def Server(self, relPath="", systemName=None):
# type: (str, str | None) -> EasyMod
""" 便捷式服务端注册 """
return self.regServer(relPath, systemName)
def Client(self, relPath="", systemName=None):
# type: (str, str | None) -> EasyMod
""" 便捷式客户端注册 """
return self.regClient(relPath, systemName)
def regClient(self, relPath="", systemName=None):
# type: (str, str | None) -> EasyMod
""" 注册客户端(相对目录) """
REG_CLIENT_MODULE("{}.{}".format(self._modDirName, relPath), systemName)
return self
def addServerInitCallFunc(self, callFunc=lambda: None):
# type: (object) -> EasyMod
""" 添加服务端初始化调用方法 """
REG_SERVER_INIT_CALL(callFunc)
return self
def addClientInitCallFunc(self, callFunc=lambda: None):
# type: (object) -> EasyMod
""" 添加客户端初始化调用方法 """
REG_CLIENT_INIT_CALL(callFunc)
return self
def START_THREAD_ANALYSIS():
""" 启用线程分析 """
_TempData._threadAnalysis = True
def STOP_THREAD_ANALYSIS():
""" 禁用线程分析 """
_TempData._threadAnalysis = False
def REG_SERVER_MODULE(absPath, systemName=None, _index=-1):
# type: (str, str | None, int) -> None
""" 注册服务端模块 (绝对路径) """
if _index < 0:
return IN.RuntimeService._serverSystemList.append((absPath, systemName))
return IN.RuntimeService._serverSystemList.insert(_index, (absPath, systemName))
def REG_CLIENT_MODULE(absPath, systemName=None, _index=-1):
# type: (str, str | None, int) -> None
""" 注册客户端模块 (绝对路径) """
if _index < 0:
return IN.RuntimeService._clientSystemList.append((absPath, systemName))
return IN.RuntimeService._clientSystemList.insert(_index, (absPath, systemName))
def REG_SERVER_INIT_CALL(func=lambda: None):
# type: (function) -> function
""" 注册服务端初始化调用函数 """
_TempData._serverInitCall.append(func)
return func
def REG_CLIENT_INIT_CALL(func=lambda: None):
# type: (function) -> function
""" 注册客户端初始化调用函数 """
_TempData._clientInitCall.append(func)
return func
def PRE_SERVER_LOADER_HOOK(func=lambda: None):
# type: (function) -> function
""" 注册服务端加载器处理前的前置逻辑 (此时依然可以注册文件 该功能用于前置关联的校验处理) """
IN.RuntimeService._serverLoadBefore.append(func)
return func
def PRE_CLIENT_LOADER_HOOK(func=lambda: None):
# type: (function) -> function
""" 注册客户端加载器处理前的前置逻辑 (此时依然可以注册文件 该功能用于前置关联的校验处理) """
IN.RuntimeService._clientLoadBefore.append(func)
return func

View File

@@ -1,185 +0,0 @@
# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi
from ...Util import errorPrint, TRY_EXEC_FUN, getObjectPathName
from ...IN import RuntimeService
from .SharedRes import (
CallObjData,
EasyListener,
SERVER_CALL_EVENT,
CLIENT_CALL_EVENT,
NAMESPACE,
SYSTEMNAME
)
lambda: "By Zero123"
ClientSystem = clientApi.GetClientSystemCls()
engineSpaceName, engineSystemName = clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName()
def clientImportModule(filePath):
""" 客户端文件导入 """
return clientApi.ImportModule(filePath)
class LoaderSystem(ClientSystem, EasyListener):
""" QuMod加载器系统
加载器承担了系统文件的加载以及事件监听 系统通信
"""
@staticmethod
def getSystem():
# type: () -> LoaderSystem
""" 获取加载器系统 如果未注册将会自动注册并返回 """
system = clientApi.GetSystem(NAMESPACE, SYSTEMNAME)
if system:
return system
return clientApi.RegisterSystem(NAMESPACE, SYSTEMNAME, LoaderSystem.__module__ + "." + LoaderSystem.__name__)
_REG_CALL_FUNCS = {}
_REG_STATIC_LISTEN_FUNCS = {}
_DY_IMP_CACHE = {}
@staticmethod
def dyImportModule(modulePath):
if not modulePath in LoaderSystem._DY_IMP_CACHE:
LoaderSystem._DY_IMP_CACHE[modulePath] = clientImportModule(modulePath)
return LoaderSystem._DY_IMP_CACHE[modulePath]
@staticmethod
def REG_DESTROY_CALL_FUNC(func=lambda: None):
""" 适用于静态函数的注册销毁时回调 """
keyName = getObjectPathName(func)
if not keyName in LoaderSystem._REG_CALL_FUNCS:
# callFunc = lambda: LoaderSystem._REG_CALL_FUNCS[keyName]()
LoaderSystem.getSystem().addDestroyCall(func)
LoaderSystem._REG_CALL_FUNCS[keyName] = func
@staticmethod
def REG_STATIC_LISTEN_FUNC(eventName="", funcObj=lambda: None):
""" 注册静态监听函数 """
keyName = getObjectPathName(funcObj)
if not keyName in LoaderSystem._REG_STATIC_LISTEN_FUNCS:
# callFunc = lambda *args: LoaderSystem._REG_STATIC_LISTEN_FUNCS[keyName](*args)
LoaderSystem.getSystem().nativeStaticListen(eventName, funcObj)
LoaderSystem._REG_STATIC_LISTEN_FUNCS[keyName] = funcObj
def __init__(self, namespace, systemName):
ClientSystem.__init__(self, namespace, systemName)
EasyListener.__init__(self)
RuntimeService._clientStarting = True
self.namespace = namespace
self.systemName = systemName
self._systemList = RuntimeService._clientSystemList
self._initState = False
self._regInitState = False
self._waitTime = 0.0
self._callQueue = [] # type: list[CallObjData]
self._onDestroyCall = []
self._onDestroyCall_LAST = []
""" 后置销毁触发 通常是内部使用确保在用户业务之后执行 """
self._initSystemListen()
self.systemInit()
def _initSystemListen(self):
self.ListenForEvent(NAMESPACE, SYSTEMNAME, SERVER_CALL_EVENT, self, self._systemCallListener)
def _easyListenForEvent(self, eventName="", parent=None, func=lambda: None):
return self.ListenForEvent(engineSpaceName, engineSystemName, eventName, parent, func)
def _easyUnListenForEvent(self, eventName="", parent=None, func=lambda: None):
return self.UnListenForEvent(engineSpaceName, engineSystemName, eventName, parent, func)
def sendCall(self, apiName="", args=tuple(), kwargs=dict()):
""" 向服务器端请求调用 """
sendData = self._packageCallArgs(apiName, args, kwargs)
self.NotifyToServer(CLIENT_CALL_EVENT, sendData)
def addDestroyCall(self, funObj, doubleCheck=True):
""" 添加销毁触发 """
if doubleCheck and funObj in self._onDestroyCall:
return
self._onDestroyCall.append(funObj)
def removeDestroyCall(self, funObj):
""" 移除销毁触发 """
if funObj in self._onDestroyCall:
self._onDestroyCall.remove(funObj)
def Destroy(self):
# 用户级destroy执行
for obj in self._onDestroyCall:
TRY_EXEC_FUN(obj)
self._onDestroyCall = []
# 高权限destroy执行
for obj in self._onDestroyCall_LAST:
TRY_EXEC_FUN(obj)
self._onDestroyCall_LAST = []
RuntimeService._clientStarting = False
def getSystemList(self):
# type: () -> list[tuple[str, str | None]]
return self._systemList
def removeCallObjByUid(self, _uid = ""):
""" 尝试移除特定uid的callObj 如果存在 """
for i, obj in enumerate(self._callQueue):
if obj._uid == _uid:
del self._callQueue[i]
break
def proxyRegister(self, funcObj):
""" 代理注册 """
from functools import wraps
@wraps(funcObj)
def newFun(*args, **kwargs):
callObj = CallObjData(funcObj, args, kwargs)
self._callQueue.append(callObj)
return callObj
return newFun
def unsafeUpdate(self, callObjData):
# type: (CallObjData) -> bool
""" 不安全的强制刷新 """
if callObjData in self._callQueue:
self._callQueue.remove(callObjData)
callObjData.callObj(*callObjData.args, **callObjData.kwargs)
return True
return False
def Update(self):
self.regSystemInit()
if self._callQueue:
for obj in self._callQueue:
try:
obj.callObj(*obj.args, **obj.kwargs)
except Exception as e:
errorPrint("{} call执行异常 {}".format(obj.callObj, e))
import traceback
traceback.print_exc()
self._callQueue = []
return ClientSystem.Update(self)
def systemInit(self):
self._initState = True
def regSystemInit(self):
""" 系统信息注册初始化 """
if self._regInitState:
return
# 加载Before事件
for funcObj in RuntimeService._clientLoadBefore:
TRY_EXEC_FUN(funcObj)
# 因历史原因systemName已废弃 此处仅兼容旧版项目
for path, _ in self._systemList:
sysObj = None
try:
sysObj = clientImportModule(path)
if sysObj == None:
errorPrint("[客户端] 系统文件加载失败(API异常): {}".format(path))
continue
except Exception as e:
errorPrint("[客户端] 系统文件错误: {} ({})".format(path, e))
import traceback
traceback.print_exc()
continue
# if not systemName: systemName = uuid4().hex
self._regInitState = True
# 加载Finish事件
for funcObj in RuntimeService._clientLoadFinish:
TRY_EXEC_FUN(funcObj)

View File

@@ -1,200 +0,0 @@
# -*- coding: utf-8 -*-
import mod.server.extraServerApi as serverApi
from ...Util import errorPrint, TRY_EXEC_FUN, getObjectPathName
from ...IN import RuntimeService
from .SharedRes import (
CallObjData,
EasyListener,
SERVER_CALL_EVENT,
CLIENT_CALL_EVENT,
NAMESPACE,
SYSTEMNAME
)
lambda: "By Zero123"
ServerSystem = serverApi.GetServerSystemCls()
engineSpaceName, engineSystemName = serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName()
def serverImportModule(filePath):
""" 服务端文件导入 """
return serverApi.ImportModule(filePath)
class LoaderSystem(ServerSystem, EasyListener):
""" QuMod加载器系统
加载器承担了系统文件的加载以及事件监听 系统通信
"""
@staticmethod
def getSystem():
# type: () -> LoaderSystem
""" 获取加载器系统 如果未注册将会自动注册并返回 """
system = serverApi.GetSystem(NAMESPACE, SYSTEMNAME)
if system:
return system
return serverApi.RegisterSystem(NAMESPACE, SYSTEMNAME, LoaderSystem.__module__ + "." + LoaderSystem.__name__)
_REG_CALL_FUNCS = {}
_REG_STATIC_LISTEN_FUNCS = {}
_DY_IMP_CACHE = {}
@staticmethod
def dyImportModule(modulePath):
if not modulePath in LoaderSystem._DY_IMP_CACHE:
LoaderSystem._DY_IMP_CACHE[modulePath] = serverImportModule(modulePath)
return LoaderSystem._DY_IMP_CACHE[modulePath]
@staticmethod
def REG_DESTROY_CALL_FUNC(func=lambda: None):
""" 适用于静态函数的注册销毁时回调 """
keyName = getObjectPathName(func)
if not keyName in LoaderSystem._REG_CALL_FUNCS:
# callFunc = lambda: LoaderSystem._REG_CALL_FUNCS[keyName]()
LoaderSystem.getSystem().addDestroyCall(func)
LoaderSystem._REG_CALL_FUNCS[keyName] = func
@staticmethod
def REG_STATIC_LISTEN_FUNC(eventName="", funcObj=lambda: None):
""" 注册静态监听函数 """
keyName = getObjectPathName(funcObj)
if not keyName in LoaderSystem._REG_STATIC_LISTEN_FUNCS:
# callFunc = lambda *args: LoaderSystem._REG_STATIC_LISTEN_FUNCS[keyName](*args)
LoaderSystem.getSystem().nativeStaticListen(eventName, funcObj)
LoaderSystem._REG_STATIC_LISTEN_FUNCS[keyName] = funcObj
def __init__(self, namespace, systemName):
ServerSystem.__init__(self, namespace, systemName)
EasyListener.__init__(self)
RuntimeService._serverStarting = True
self.namespace = namespace
self.systemName = systemName
self.rpcPlayerId = None
self._systemList = RuntimeService._serverSystemList
self._initState = False
self._regInitState = False
self._waitTime = 0.0
self._onDestroyCall = []
self._onDestroyCall_LAST = []
""" 后置销毁触发 通常是内部使用确保在用户业务之后执行 """
self._initSystemListen()
self.systemInit()
def _systemCallListenerHook(self, args={}):
target = "__id__"
if target in args:
self.rpcPlayerId = args[target]
return
self.rpcPlayerId = None
def _initSystemListen(self):
self.ListenForEvent(NAMESPACE, SYSTEMNAME, CLIENT_CALL_EVENT, self, self._systemCallListener)
def _easyListenForEvent(self, eventName="", parent=None, func=lambda: None):
return self.ListenForEvent(engineSpaceName, engineSystemName, eventName, parent, func)
def _easyUnListenForEvent(self, eventName="", parent=None, func=lambda: None):
return self.UnListenForEvent(engineSpaceName, engineSystemName, eventName, parent, func)
def sendCall(self, playerId="", apiName="", args=tuple(), kwargs=dict()):
""" 向指定玩家客户端请求调用 当playerId声明为*时代表全体玩家 """
sendData = self._packageCallArgs(apiName, args, kwargs)
if playerId == "*":
self.BroadcastToAllClient(SERVER_CALL_EVENT, sendData)
return
self.NotifyToClient(playerId, SERVER_CALL_EVENT, sendData)
def sendMultiClientsCall(self, playerListId=[], apiName="", args=tuple(), kwargs=dict()):
""" 批量向多个玩家客户端发包相同的调用数据 """
sendData = self._packageCallArgs(apiName, args, kwargs)
self.NotifyToMultiClients(playerListId, SERVER_CALL_EVENT, sendData)
def addDestroyCall(self, funObj, doubleCheck=True):
""" 添加销毁触发 """
if doubleCheck and funObj in self._onDestroyCall:
return
self._onDestroyCall.append(funObj)
def removeDestroyCall(self, funObj):
""" 移除销毁触发 """
if funObj in self._onDestroyCall:
self._onDestroyCall.remove(funObj)
def Destroy(self):
# 用户级destroy执行
for obj in self._onDestroyCall:
TRY_EXEC_FUN(obj)
self._onDestroyCall = []
# 高权限destroy执行
for obj in self._onDestroyCall_LAST:
TRY_EXEC_FUN(obj)
self._onDestroyCall_LAST = []
RuntimeService._serverStarting = False
def getSystemList(self):
# type: () -> list[tuple[str, str | None]]
return self._systemList
def removeCallObjByUid(self, _uid = ""):
""" 尝试移除特定uid的callObj 如果存在 """
for i, obj in enumerate(self._callQueue):
if obj._uid == _uid:
del self._callQueue[i]
break
def proxyRegister(self, funcObj):
""" 代理注册 """
from functools import wraps
@wraps(funcObj)
def newFun(*args, **kwargs):
callObj = CallObjData(funcObj, args, kwargs)
self._callQueue.append(callObj)
return callObj
return newFun
def unsafeUpdate(self, callObjData):
# type: (CallObjData) -> bool
""" 不安全的强制刷新 """
if callObjData in self._callQueue:
self._callQueue.remove(callObjData)
callObjData.callObj(*callObjData.args, **callObjData.kwargs)
return True
return False
def Update(self):
self.regSystemInit()
if self._callQueue:
for obj in self._callQueue:
try:
obj.callObj(*obj.args, **obj.kwargs)
except Exception as e:
errorPrint("{} call执行异常 {}".format(obj.callObj, e))
import traceback
traceback.print_exc()
self._callQueue = []
return ServerSystem.Update(self)
def systemInit(self):
self._initState = True
def regSystemInit(self):
""" 系统信息注册初始化 """
if self._regInitState:
return
# 加载Before事件
for funcObj in RuntimeService._serverLoadBefore:
TRY_EXEC_FUN(funcObj)
# 因历史原因systemName已废弃 此处仅兼容旧版项目
for path, _ in self._systemList:
sysObj = None
try:
sysObj = serverImportModule(path)
if sysObj == None:
errorPrint("[服务端] 系统文件加载失败(API异常): {}".format(path))
continue
except Exception as e:
errorPrint("[服务端] 系统文件错误: {} ({})".format(path, e))
import traceback
traceback.print_exc()
continue
# if not systemName: systemName = uuid4().hex
self._regInitState = True
# 加载Finish事件
for funcObj in RuntimeService._serverLoadFinish:
TRY_EXEC_FUN(funcObj)

View File

@@ -1,175 +0,0 @@
# -*- coding: utf-8 -*-
from uuid import uuid4
from ...IN import ModDirName
from ...Util import QStruct
class CallObjData:
def __init__(self, callObj, args = tuple(), kwargs = {}):
self.callObj = callObj
self.args = args
self.kwargs = kwargs
self._uid = None
class EmptyContext:
pass
NAMESPACE = "Qu_" + ModDirName
SYSTEMNAME = "{}_QLoader_system".format(ModDirName)
SERVER_CALL_EVENT = "{}_QServer".format(ModDirName)
CLIENT_CALL_EVENT = "{}_QClient".format(ModDirName)
class EasyListener:
def __init__(self):
self._callQueue = [] # type: list[CallObjData]
self._emptyContext = EmptyContext()
self._QCustomAPI = {} # type: dict[str, function]
def regCustomApi(self, apiName="", func=lambda: None):
""" 注册自定义API """
self._QCustomAPI[apiName] = func
def removeCustomApi(self, apiName=""):
""" 删除指定API如果存在 """
if apiName in self._QCustomAPI:
del self._QCustomAPI[apiName]
def getCustomApi(self, apiName=""):
""" 获取自定义API如果存在 """
return self._QCustomAPI.get(apiName)
def localCall(self, apiName="", *args, **kwargs):
""" 本地调用 请确保API函数存在注册 否则抛出异常 """
return self._QCustomAPI[apiName](*args, **kwargs)
def _systemCallListener(self, args={}):
""" 系统call机制监听器(接收消息处理) """
api = args["api"]
ag = EasyListener._unPackRefArgs(args["args"])
kwargs = EasyListener._unPackRefDictArgs(args["kw"])
self._systemCallListenerHook(args)
return self.localCall(api, *ag, **kwargs)
def _systemCallListenerHook(self, _={}):
pass
@staticmethod
def _unPackRefArgs(data):
# type: (list) -> list
""" Ref解包Args数据 """
for i, v in enumerate(data):
if QStruct.isSignData(v):
data[i] = QStruct.loadSignData(v).onNetUnPack()
return data
@staticmethod
def _unPackRefDictArgs(data):
# type: (dict) -> dict
""" Ref解包Dict Args数据 """
for k, v in data.items():
if QStruct.isSignData(v):
data[k] = QStruct.loadSignData(v).onNetUnPack()
return data
@staticmethod
def _packArgs(data):
# type: (tuple | list) -> list
""" 打包Args数据 """
newDataList = []
for v in data:
if isinstance(v, QStruct):
newDataList.append(v.signDumps())
continue
newDataList.append(v)
return newDataList
@staticmethod
def _packDictArgs(data):
# type: (dict) -> dict
""" 打包Dict数据 keyName=xxx """
newDict = {}
for k, v in data.items():
if isinstance(v, QStruct):
newDict[k] = v.signDumps()
continue
newDict[k] = v
return newDict
def _packageCallArgs(self, apiName="", args=tuple(), kwargs=dict()):
""" 打包API参数(发送消息处理) """
return {"api":apiName,"args":EasyListener._packArgs(args),"kw":EasyListener._packDictArgs(kwargs)}
def mallocRandomMetName(self):
""" 动态分配随机方法名 """
randomName = ""
while not randomName or hasattr(self, randomName):
randomName = "Q_{}".format(uuid4().hex)
return randomName
def _allocMethodWithOUTFunction(self, callFunc=lambda *_: None):
""" 基于外部函数分配一个映射的内部方法(介于网易Listen必须依赖内部方法 故有了该方法) """
newFuncName = self.mallocRandomMetName()
newFunc = lambda *args, **kwargs: callFunc(*args, **kwargs)
newFunc.__name__ = newFuncName
setattr(self, newFuncName, newFunc)
return newFunc
def _delMethod(self, methodFunc=lambda *_: None):
""" 对照与_allocMethodWithOUTFunction的反向删除 """
try:
delattr(self, methodFunc.__name__)
except Exception:
pass
def nativeStaticListen(self, eventName="", callFunc=lambda *_: None):
""" 原生静态监听注册 不支持运行时注销 """
def _reg():
self._easyListenForEvent(eventName, self, self._allocMethodWithOUTFunction(callFunc))
self._callQueue.append(CallObjData(_reg))
def nativeListen(self, eventName="", parent=None, callFunc=lambda *_: None, updateNow=False):
# type: (str, object, object, bool) -> CallObjData | None
""" 原生动态监听 当updateNow声明为False时将会添加到系统队列安全的等待注册 """
if not parent:
parent = self._emptyContext
newFuncName = "QListen{}_{}".format(id(parent), callFunc.__name__)
newFunc = lambda *args: callFunc(*args)
newFunc.__name__ = newFuncName
if hasattr(self, newFuncName):
print("[Error] 请勿在单个可执行对象上监听重复的事件")
return None
def _reg():
setattr(self, newFuncName, newFunc)
self._easyListenForEvent(eventName, self, newFunc)
waitCallObj = CallObjData(_reg)
waitCallObj._uid = newFuncName
self._callQueue.append(waitCallObj)
if updateNow:
self.unsafeUpdate(waitCallObj)
return waitCallObj
def unNativeListen(self, eventName="", parent=None, callFunc=lambda *_: None):
# type: (str, object, object) -> None
""" 取消特定方法的原生动态监听 """
if not parent:
parent = self._emptyContext
newFuncName = "QListen{}_{}".format(id(parent), callFunc.__name__)
if hasattr(self, newFuncName):
# 已注册完毕的监听处理
self._easyUnListenForEvent(eventName, self, getattr(self, newFuncName))
delattr(self, newFuncName)
return
# 在队列中等待注册的监听处理
self.removeCallObjByUid(newFuncName)
def unsafeUpdate(self, callObjData):
# type: (CallObjData) -> bool
pass
def _easyListenForEvent(self, eventName="", parent=None, func=lambda: None):
pass
def _easyUnListenForEvent(self, eventName="", parent=None, func=lambda: None):
pass
def removeCallObjByUid(self, _uid = ""):
pass

View File

@@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
from functools import wraps
from time import time
import pickle as _pickle
class SystemSide(object):
def __init__(self, Path, SystemName = None):
self.SystemName = SystemName # 绑定系统
self.Path = Path
ModDirName = SystemSide.__module__.split(".")[0]
def errorPrint(charPtr):
""" 异常输出 """
print("[Error] "+str(charPtr))
def TRY_EXEC_FUN(funObj, *args, **kwargs):
try:
return funObj(*args, **kwargs)
except Exception as e:
import traceback
print("TRY_EXEC发生异常: {}".format(e))
traceback.print_exc()
def getObjectPathName(_callObj = lambda: None):
# type: (object) -> str
""" 获取可执行对象的目录名 """
return ".".join((_callObj.__module__, _callObj.__name__))
class QStruct:
""" 结构体 用于通用数据模型约定(即不涉及任何API) 应定义在Server/Client以外的通用文件 同理Struct也不应该持有任何涉及端侧API的内容 """
_SIGN_FORMAT = "_QSTRUCT[{}]"
def dumps(self):
""" 序列化对象 """
return _pickle.dumps(self)
def signDumps(self):
""" 带有特征签名的序列化 """
data = self.dumps()
return [QStruct._SIGN_FORMAT.format(hex(hash(data))), data]
@staticmethod
def isSignData(data):
""" 校验数据 """
if not isinstance(data, list) or len(data) != 2:
return False
signKey = data[0]
if isinstance(signKey, str):
dataObj = data[1]
return signKey == QStruct._SIGN_FORMAT.format(hex(hash(dataObj)))
return False
@staticmethod
def loads(data):
# type: (str) -> QStruct
""" 反序列化加载对象 """
return _pickle.loads(data)
@staticmethod
def loadSignData(data):
# type: (list) -> QStruct
""" 反序列化加载Sign对象表(不会校验) """
return _pickle.loads(data[1])
def onNetUnPack(self):
return self
class QRefStruct(QStruct):
""" 万能引用 """
def __init__(self, refObject):
self.ref = refObject
def onNetUnPack(self):
return self.ref
class QListStruct(QStruct, list):
""" List容器结构 """
def onNetUnPack(self):
return list(self)
class QDictStruct(QStruct, dict):
""" Dict容器结构 """
def onNetUnPack(self):
return dict(self)
class QTupleStruct(QStruct, tuple):
""" Tuple容器结构 """
def onNetUnPack(self):
return tuple(self)

View File

@@ -1,128 +1,44 @@
# -*- coding: utf-8 -*-
from .QuModLibs.QuMod import *
from .Game import (
RELOAD_MOD,
INIT_RELOAD_TIME,
RELOAD_ADDON,
RELOAD_WORLD,
RELOAD_SHADERS,
from mod.common.mod import Mod
import mod.client.extraClientApi as clientApi
import mod.server.extraServerApi as serverApi
from .Lifecycle import install_output_wrapper
MOD_NAME = "EmodCliDebug"
MOD_VERSION = "1.0.0"
SERVER_SYSTEM_NAME = "EmodCliDebugServer"
SERVER_SYSTEM_PATH = "DEBUG_ENV_SCRIPT.DebugServerSystem.DebugServerSystem"
CLIENT_SYSTEM_NAME = "EmodCliDebugClient"
CLIENT_SYSTEM_PATH = "DEBUG_ENV_SCRIPT.DebugClientSystem.DebugClientSystem"
install_output_wrapper()
@Mod.Binding(name=MOD_NAME, version=MOD_VERSION)
class EmodCliDebugMod(object):
@Mod.InitServer()
def server_init(self):
serverApi.RegisterSystem(
MOD_NAME,
SERVER_SYSTEM_NAME,
SERVER_SYSTEM_PATH,
)
from .Config import DEBUG_CONFIG
import threading
import sys
lambda: "By Zero123"
@Mod.InitClient()
def client_init(self):
clientApi.RegisterSystem(
MOD_NAME,
CLIENT_SYSTEM_NAME,
CLIENT_SYSTEM_PATH,
)
REF = 0
class STD_OUT_WRAPPER(object):
def __init__(self, baseIO):
self.baseIO = baseIO
self.writeLock = threading.Lock()
self._buffer = []
def __getattr__(self, name):
return getattr(self.baseIO, name)
def write(self, data):
with self.writeLock:
parts = data.splitlines(True)
for part in parts:
if part.endswith("\n"):
if self._buffer:
line = "".join(self._buffer) + part
self._buffer = []
else:
line = part
self.baseIO.write("[Python] " + line)
else:
self._buffer.append(part)
def close(self):
return self.baseIO.close()
def writelines(self, lines):
for line in lines:
self.write(line)
def fileno(self):
return self.baseIO.fileno()
stdout = sys.stdout
stderr = sys.stderr
def REST_STDOUT():
sys.stdout = stdout
sys.stderr = stderr
sys.stdout = STD_OUT_WRAPPER(sys.stdout)
sys.stderr = STD_OUT_WRAPPER(sys.stderr)
@PRE_SERVER_LOADER_HOOK
def SERVER_INIT():
global REF
REF += 1
def _DESTROY():
global REF
REF -= 1
if REF != 0:
return
REST_STDOUT()
from .QuModLibs.Systems.Loader.Server import LoaderSystem
LoaderSystem.REG_DESTROY_CALL_FUNC(_DESTROY)
from . import IPCSystem
IPCSystem.ON_SERVER_INIT()
def CLOnKeyPressInGame(args={}):
if args["isDown"] != "0":
return
if args["screenName"] != "hud_screen" and not DEBUG_CONFIG.get(
"reload_key_global", False
):
return
key = args["key"]
if key == str(DEBUG_CONFIG.get("reload_key", "82")):
RELOAD_MOD()
elif key == str(DEBUG_CONFIG.get("reload_world_key", "")):
RELOAD_WORLD()
elif key == str(DEBUG_CONFIG.get("reload_addon_key", "")):
RELOAD_ADDON()
elif key == str(DEBUG_CONFIG.get("reload_shaders_key", "")):
RELOAD_SHADERS()
@PRE_CLIENT_LOADER_HOOK
def CLIENT_INIT():
global REF
REF += 1
from . import IPCSystem
def _DESTROY():
global REF
REF -= 1
IPCSystem.ON_CLIENT_EXIT()
if REF != 0:
return
REST_STDOUT()
from .QuModLibs.Systems.Loader.Client import LoaderSystem
LoaderSystem.REG_DESTROY_CALL_FUNC(_DESTROY)
LoaderSystem.getSystem().nativeStaticListen("OnKeyPressInGame", CLOnKeyPressInGame)
IPCSystem.ON_CLIENT_INIT()
try:
INIT_RELOAD_TIME()
except:
@Mod.DestroyServer()
def server_destroy(self):
pass
@Mod.DestroyClient()
def client_destroy(self):
pass