505 lines
16 KiB
Python
505 lines
16 KiB
Python
import sys
|
|
import os
|
|
import builtins
|
|
import time
|
|
import _thread
|
|
import gc
|
|
import socket
|
|
import struct
|
|
import warnings
|
|
|
|
# === GLOBAL WARNING SUPPRESSION & METADATA SHIM (very early) ===
|
|
try:
|
|
import importlib.metadata
|
|
_orig_metadata_version = importlib.metadata.version
|
|
_orig_metadata_distribution = importlib.metadata.distribution
|
|
|
|
_KNOWN_VERSIONS = {
|
|
"werkzeug": "3.1.9",
|
|
"flask": "3.1.3",
|
|
"marshmallow": "3.20.0",
|
|
"requests": "2.34.2",
|
|
"urllib3": "2.8.0",
|
|
"protobuf": "7.36.2",
|
|
"click": "8.5.0",
|
|
"jinja2": "3.1.6",
|
|
"itsdangerous": "2.2.0",
|
|
"blinker": "1.9.0",
|
|
"markupsafe": "3.0.4",
|
|
"dataclasses-json": "0.6.7",
|
|
"dataclasses_json": "0.6.7",
|
|
"packaging": "26.3"
|
|
}
|
|
|
|
class _ShimedDistribution:
|
|
def __init__(self, name, version):
|
|
self.name = name
|
|
self.version = version
|
|
self.metadata = {"Name": name, "Version": version}
|
|
|
|
def _safe_metadata_version(name):
|
|
try:
|
|
return _orig_metadata_version(name)
|
|
except Exception:
|
|
clean = name.lower().replace("-", "_")
|
|
return _KNOWN_VERSIONS.get(name.lower(), _KNOWN_VERSIONS.get(clean, "1.0.0"))
|
|
|
|
def _safe_metadata_distribution(name):
|
|
try:
|
|
return _orig_metadata_distribution(name)
|
|
except Exception:
|
|
ver = _safe_metadata_version(name)
|
|
return _ShimedDistribution(name, ver)
|
|
|
|
importlib.metadata.version = _safe_metadata_version
|
|
importlib.metadata.distribution = _safe_metadata_distribution
|
|
except Exception:
|
|
pass
|
|
|
|
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
|
|
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
|
|
warnings.filterwarnings("ignore", module="marshmallow.*")
|
|
warnings.filterwarnings("ignore", module="dataclasses_json.*")
|
|
|
|
try:
|
|
from marshmallow.warnings import RemovedInMarshmallow4Warning
|
|
warnings.filterwarnings("ignore", category=RemovedInMarshmallow4Warning)
|
|
warnings.simplefilter("ignore", category=RemovedInMarshmallow4Warning)
|
|
print("[Shim] Global warning suppression activated")
|
|
except Exception:
|
|
pass
|
|
|
|
_active_server_instance = None
|
|
|
|
# --- 1. Stream Redirector for Android UI ---
|
|
class AndroidLogStream:
|
|
def __init__(self, activity_context, original_stream):
|
|
self.activity_context = activity_context
|
|
self.original_stream = original_stream
|
|
self.buffer = ""
|
|
self._in_write = False
|
|
|
|
def write(self, message):
|
|
if self._in_write:
|
|
if self.original_stream:
|
|
try:
|
|
self.original_stream.write(message)
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
self._in_write = True
|
|
try:
|
|
if self.original_stream:
|
|
try:
|
|
self.original_stream.write(message)
|
|
except Exception:
|
|
pass
|
|
self.buffer += message
|
|
while "\n" in self.buffer:
|
|
line, self.buffer = self.buffer.split("\n", 1)
|
|
clean_line = line.strip()
|
|
if clean_line and self.activity_context:
|
|
try:
|
|
self.activity_context.onServerLog(clean_line)
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
self._in_write = False
|
|
|
|
def flush(self):
|
|
if self.original_stream:
|
|
try:
|
|
self.original_stream.flush()
|
|
except Exception:
|
|
pass
|
|
if self.buffer.strip() and self.activity_context:
|
|
try:
|
|
self.activity_context.onServerLog(self.buffer.strip())
|
|
self.buffer = ""
|
|
except Exception:
|
|
pass
|
|
|
|
# --- 2. Headless Input Mock ---
|
|
def _mock_input(prompt=""):
|
|
if prompt:
|
|
print(f"[CLI]: {prompt}")
|
|
time.sleep(2)
|
|
return ""
|
|
|
|
builtins.input = _mock_input
|
|
|
|
# --- 3. Minimal Marshmallow Info ---
|
|
try:
|
|
import marshmallow
|
|
print(f"[Shim] marshmallow {marshmallow.__version__}")
|
|
except Exception:
|
|
print("[Shim] marshmallow not available")
|
|
|
|
# --- 3.1 Persistent Template & SafeDict Protection ---
|
|
DEFAULT_PERSISTENT = {
|
|
"send_hotfix": True,
|
|
"scene": {
|
|
"plane_id": 20313,
|
|
"x": 40748,
|
|
"y": 192819,
|
|
"z": 439218,
|
|
"calyx_x": 40748,
|
|
"calyx_y": 192810,
|
|
"calyx_z": 441000,
|
|
"map_layer": 2,
|
|
"calyx_group_id": 186,
|
|
"calyx_inst_id": 300001,
|
|
"calyx_entity_id": 1337,
|
|
"calyx_prop_id": 808
|
|
},
|
|
"avatar": {
|
|
"tb_gender": "Female",
|
|
"tb_path": "Elation",
|
|
"march_path": "Preservation",
|
|
"skin": {
|
|
"1001": 1100101,
|
|
"1310": 1131001,
|
|
"1415": 1141501,
|
|
"1407": 1140701,
|
|
"1501": 1150101
|
|
},
|
|
"lineup": [
|
|
1511,
|
|
1502,
|
|
1506,
|
|
1503
|
|
],
|
|
"global_passive": [
|
|
{
|
|
"name": "Castorice Global Passive",
|
|
"id": 140703,
|
|
"active": True
|
|
},
|
|
{
|
|
"name": "SW999 Global Passsive",
|
|
"id": 150602,
|
|
"active": True
|
|
},
|
|
{
|
|
"name": "Aha Global Passive",
|
|
"id": 151104,
|
|
"active": True
|
|
}
|
|
],
|
|
"ex_rank": [
|
|
{
|
|
"name": "JingYuan",
|
|
"avatar_id": 1204,
|
|
"active": True
|
|
},
|
|
{
|
|
"name": "DanHeng IL",
|
|
"avatar_id": 1213,
|
|
"active": True
|
|
},
|
|
{
|
|
"name": "Acheron",
|
|
"avatar_id": 1308,
|
|
"active": True
|
|
}
|
|
],
|
|
"buff": [
|
|
1000121
|
|
],
|
|
"leader": 0
|
|
},
|
|
"key": ""
|
|
}
|
|
|
|
class SafeDict(dict):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for k, v in list(self.items()):
|
|
if isinstance(v, dict) and not isinstance(v, SafeDict):
|
|
self[k] = SafeDict(v)
|
|
|
|
def __getitem__(self, key):
|
|
import copy
|
|
if key not in self:
|
|
if key == 'avatar':
|
|
self['avatar'] = SafeDict(copy.deepcopy(DEFAULT_PERSISTENT['avatar']))
|
|
return self['avatar']
|
|
elif key == 'scene':
|
|
self['scene'] = SafeDict(copy.deepcopy(DEFAULT_PERSISTENT['scene']))
|
|
return self['scene']
|
|
elif key == 'skin':
|
|
self['skin'] = SafeDict(copy.deepcopy(DEFAULT_PERSISTENT['avatar']['skin']))
|
|
return self['skin']
|
|
elif key == 'lineup':
|
|
self['lineup'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['lineup'])
|
|
return self['lineup']
|
|
elif key == 'buff':
|
|
self['buff'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['buff'])
|
|
return self['buff']
|
|
elif key == 'global_passive':
|
|
self['global_passive'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['global_passive'])
|
|
return self['global_passive']
|
|
elif key == 'ex_rank':
|
|
self['ex_rank'] = copy.deepcopy(DEFAULT_PERSISTENT['avatar']['ex_rank'])
|
|
return self['ex_rank']
|
|
elif key == 'tb_gender':
|
|
return 'Female'
|
|
elif key == 'tb_path':
|
|
return 'Elation'
|
|
elif key == 'march_path':
|
|
return 'Preservation'
|
|
elif key == 'leader':
|
|
return 0
|
|
elif key == 'send_hotfix':
|
|
return True
|
|
elif key == 'key':
|
|
return ''
|
|
val = super().__getitem__(key)
|
|
if isinstance(val, dict) and not isinstance(val, SafeDict):
|
|
val = SafeDict(val)
|
|
self[key] = val
|
|
return val
|
|
|
|
def _sanitize_persistent_file(persistent_file):
|
|
try:
|
|
import json, os, copy
|
|
os.makedirs(os.path.dirname(os.path.abspath(persistent_file)), exist_ok=True)
|
|
data = {}
|
|
if os.path.exists(persistent_file):
|
|
try:
|
|
with open(persistent_file, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except Exception:
|
|
data = {}
|
|
|
|
modified = False
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
modified = True
|
|
|
|
for k, v in DEFAULT_PERSISTENT.items():
|
|
if k not in data:
|
|
data[k] = copy.deepcopy(v)
|
|
modified = True
|
|
elif isinstance(v, dict) and isinstance(data[k], dict):
|
|
for sub_k, sub_v in v.items():
|
|
if sub_k not in data[k]:
|
|
data[k][sub_k] = copy.deepcopy(sub_v)
|
|
modified = True
|
|
|
|
if modified or not os.path.exists(persistent_file):
|
|
with open(persistent_file, "w", encoding="utf-8") as f:
|
|
json.dump(data, f, indent=4)
|
|
print("[Patch] persistent.json validated and auto-healed with complete avatar/scene defaults")
|
|
except Exception as e:
|
|
print(f"[Patch Warning] sanitize persistent: {e}")
|
|
|
|
# --- 4. Shutdown Logic ---
|
|
def kill_server_process():
|
|
global _active_server_instance
|
|
print("[Shutdown]: Force-stopping server execution...")
|
|
|
|
try:
|
|
import dispatch.dispatchserver as _ds
|
|
if hasattr(_ds, 'DispatchServer') and hasattr(_ds.DispatchServer, 'stop'):
|
|
_ds.DispatchServer.stop()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
import game.gameserver as _gs
|
|
if hasattr(_gs, 'GameServer') and hasattr(_gs.GameServer, 'stop'):
|
|
_gs.GameServer.stop()
|
|
except Exception:
|
|
pass
|
|
|
|
if _active_server_instance is not None:
|
|
try:
|
|
if hasattr(_active_server_instance, 'shutdown'):
|
|
_active_server_instance.shutdown()
|
|
if hasattr(_active_server_instance, 'server_close'):
|
|
_active_server_instance.server_close()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
sock = getattr(_active_server_instance, 'socket', None)
|
|
if sock:
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0))
|
|
sock.close()
|
|
except Exception:
|
|
pass
|
|
_active_server_instance = None
|
|
|
|
try:
|
|
for obj in gc.get_objects():
|
|
if isinstance(obj, socket.socket):
|
|
try:
|
|
obj.shutdown(socket.SHUT_RDWR)
|
|
obj.close()
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
s.close()
|
|
except Exception:
|
|
pass
|
|
|
|
time.sleep(1.2)
|
|
|
|
for _ in range(5):
|
|
try:
|
|
_thread.interrupt_main()
|
|
time.sleep(0.2)
|
|
except Exception:
|
|
pass
|
|
|
|
# --- 5. Main Execution Entrypoint ---
|
|
def run_server(ppsr_path, activity_context=None):
|
|
if activity_context is not None:
|
|
sys.stdout = AndroidLogStream(activity_context, sys.__stdout__)
|
|
sys.stderr = AndroidLogStream(activity_context, sys.__stderr__)
|
|
|
|
# Extra safety - re-apply on every start
|
|
try:
|
|
warnings.filterwarnings("ignore", category=RemovedInMarshmallow4Warning)
|
|
except Exception:
|
|
pass
|
|
|
|
ppsr_dir = os.path.dirname(os.path.abspath(ppsr_path))
|
|
misc_dir = os.path.join(ppsr_dir, "misc")
|
|
pkg_zip = os.path.join(ppsr_dir, "packages.zip")
|
|
|
|
if os.path.exists(ppsr_dir):
|
|
os.chdir(ppsr_dir)
|
|
print(f"Working directory locked: {os.getcwd()}")
|
|
|
|
for path in [ppsr_dir, misc_dir, pkg_zip]:
|
|
if os.path.exists(path) and path not in sys.path:
|
|
sys.path.insert(0, path)
|
|
|
|
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
|
|
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
|
|
warnings.filterwarnings("ignore", module="marshmallow.*")
|
|
warnings.filterwarnings("ignore", module="dataclasses_json.*")
|
|
|
|
# Verify and auto-recover critical packages if needed
|
|
for pkg_name in ["requests", "werkzeug", "flask"]:
|
|
try:
|
|
__import__(pkg_name)
|
|
except ImportError:
|
|
print(f"[Shim] Package '{pkg_name}' missing, attempting auto-install...")
|
|
try:
|
|
import package_fetcher
|
|
package_fetcher.install_package(pkg_name, ppsr_dir)
|
|
except Exception as ex:
|
|
print(f"[Shim Warning] Fallback failed for {pkg_name}: {ex}")
|
|
|
|
# --- Werkzeug Patch ---
|
|
try:
|
|
import werkzeug.serving
|
|
if hasattr(werkzeug.serving, "ThreadedWSGIServer"):
|
|
try:
|
|
werkzeug.serving.ThreadedWSGIServer._server_version = "Werkzeug/3.1.9"
|
|
except Exception:
|
|
pass
|
|
|
|
_orig_make_server = werkzeug.serving.make_server
|
|
|
|
def _patched_make_server(*args, **kwargs):
|
|
global _active_server_instance
|
|
kwargs.pop('options', None)
|
|
server = _orig_make_server(*args, **kwargs)
|
|
|
|
try:
|
|
server._server_version = "Werkzeug/3.1.9"
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
if hasattr(server, 'socket') and server.socket:
|
|
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
except Exception:
|
|
pass
|
|
|
|
_active_server_instance = server
|
|
print("[Server] Server instance patched and ready")
|
|
return server
|
|
|
|
werkzeug.serving.make_server = _patched_make_server
|
|
print("[Patch] Werkzeug make_server patched successfully")
|
|
|
|
except Exception as e:
|
|
print(f"[Patch Warning] Werkzeug patch: {e}")
|
|
|
|
# --- Persistent & Avatar Auto-healing Patch ---
|
|
_persistent_file = os.path.join(misc_dir, "persistent.json")
|
|
_sanitize_persistent_file(_persistent_file)
|
|
|
|
try:
|
|
import game.services.manager as _mgr_mod
|
|
_orig_load_persistent = _mgr_mod.Manager.load_persistent
|
|
|
|
@classmethod
|
|
def _patched_load_persistent(cls):
|
|
_orig_load_persistent()
|
|
if hasattr(cls, 'persistent') and isinstance(cls.persistent, dict):
|
|
cls.persistent = SafeDict(cls.persistent)
|
|
|
|
_mgr_mod.Manager.load_persistent = _patched_load_persistent
|
|
if hasattr(_mgr_mod.Manager, 'persistent') and isinstance(_mgr_mod.Manager.persistent, dict):
|
|
_mgr_mod.Manager.persistent = SafeDict(_mgr_mod.Manager.persistent)
|
|
print("[Patch] Manager.load_persistent patched with SafeDict avatar protection")
|
|
except Exception as e:
|
|
print(f"[Patch Warning] Manager persistent patch: {e}")
|
|
|
|
try:
|
|
import game.router as _router_mod
|
|
_orig_router_handle = _router_mod.handle
|
|
|
|
def _safe_router_handle(data):
|
|
try:
|
|
return _orig_router_handle(data)
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"[Router Warning] Handled exception in router.handle: {e}")
|
|
traceback.print_exc()
|
|
return b''
|
|
|
|
_router_mod.handle = _safe_router_handle
|
|
print("[Patch] game.router.handle wrapped with crash-guard")
|
|
except Exception as e:
|
|
print(f"[Patch Warning] Router crash-guard: {e}")
|
|
|
|
print(f"Loading bytecode target: {ppsr_path}...")
|
|
|
|
with open(ppsr_path, "rb") as f:
|
|
code_bytes = f.read()
|
|
|
|
try:
|
|
import marshal
|
|
code_obj = marshal.loads(code_bytes[16:])
|
|
except Exception:
|
|
code_obj = compile(code_bytes, ppsr_path, "exec")
|
|
|
|
main_globals = {
|
|
"__name__": "__main__",
|
|
"__file__": ppsr_path,
|
|
"__doc__": None,
|
|
"sys": sys,
|
|
"os": os,
|
|
"input": _mock_input
|
|
}
|
|
|
|
try:
|
|
exec(code_obj, main_globals)
|
|
except KeyboardInterrupt:
|
|
print("[System]: Server loop interrupted cleanly.")
|
|
except Exception as e:
|
|
print(f"[System]: Server terminated: {e}")
|