Files
GaCyGO-Android/app/src/main/python/run_ppsr.py
T

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