fix readme

This commit is contained in:
CAtlas0784 committed 2026-11-02 02:55:25 +07:00
1 parent 51a2ca3f8c
commit 61a03b5957
22 files changed
+3234 -71

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
Generated
+1
View File
@@ -0,0 +1 @@
GaCy-Android
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
+123
View File
@@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+1 -1
View File
@@ -1,6 +1,6 @@
# GaCy Android # GaCy Android
[ภาษาไทย](README.md) | English | [简体中文](README_ZH.md) [ภาษาไทย](./README.md) | English | [简体中文](./README_ZH.md)
GaCy Android is an Android companion and local server manager for the GaCy Android is an Android companion and local server manager for the
GaC-hkrpg-go project. The GaC-hkrpg-go project. The
+1 -1
View File
@@ -1,6 +1,6 @@
# GaCy Android # GaCy Android
[ภาษาไทย](README.md) | [English](README_EN.md) | 简体中文 [ภาษาไทย](README.md) | [English](./README_EN.md) | 简体中文
GaCy Android 是 GaC-hkrpg-go GaCy Android 是 GaC-hkrpg-go
的 Android 辅助应用和本地服务器管理器。移植仍在进行中:应用可通过前台服务启动 的 Android 辅助应用和本地服务器管理器。移植仍在进行中:应用可通过前台服务启动
+1 -1
View File
@@ -56,7 +56,7 @@ Built with **Kotlin**, **Jetpack Compose**, and **Material Design 3**.
[![Target ABI](https://img.shields.io/badge/ABI-arm64--v8a-blue?style=flat)](https://developer.android.com/ndk) [![Target ABI](https://img.shields.io/badge/ABI-arm64--v8a-blue?style=flat)](https://developer.android.com/ndk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[ภาษาไทย](README.md) | [English](README_EN.md) | [简体中文](README_ZH.md) ภาษาไทย | [English](./Docs/README_EN.md) | [简体中文](./Docs/README_ZH.md)
</div> </div>
Binary file not shown.
+22 -22
View File
@@ -1,23 +1,23 @@
{ {
"assets": { "assets": {
"bootstrap-native/arm64-v8a/_struct.cpython-314-aarch64-linux-android.so": "9b083e7b97e92291f73d80d7495c9cee8351596b", "bootstrap-native/arm64-v8a/_struct.cpython-314-aarch64-linux-android.so": "9b083e7b97e92291f73d80d7495c9cee8351596b",
"requirements-arm64-v8a.imy": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33", "requirements-arm64-v8a.imy": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
"bootstrap-native/arm64-v8a/java/chaquopy.so": "1284a42a1b74c8fc5c8705a4f6f309134ecef4a8", "bootstrap-native/arm64-v8a/java/chaquopy.so": "1284a42a1b74c8fc5c8705a4f6f309134ecef4a8",
"bootstrap.imy": "6c6d7c85e4764e96924001f9d6290501bfbfc704", "bootstrap.imy": "6c6d7c85e4764e96924001f9d6290501bfbfc704",
"bootstrap-native/arm64-v8a/math.cpython-314-aarch64-linux-android.so": "dc859c93d3fc01f2d087471052895f230f732436", "bootstrap-native/arm64-v8a/math.cpython-314-aarch64-linux-android.so": "dc859c93d3fc01f2d087471052895f230f732436",
"cacert.pem": "fec7b8c0a17c1c555fd33baee53678f31b7645ee", "cacert.pem": "fec7b8c0a17c1c555fd33baee53678f31b7645ee",
"bootstrap-native/arm64-v8a/_bz2.cpython-314-aarch64-linux-android.so": "b48b9abf1f87d587a5f5beb4b38bb17acf29c4d3", "bootstrap-native/arm64-v8a/_bz2.cpython-314-aarch64-linux-android.so": "b48b9abf1f87d587a5f5beb4b38bb17acf29c4d3",
"bootstrap-native/arm64-v8a/_ctypes.cpython-314-aarch64-linux-android.so": "50a6a36dab6a9ebe4fca5797872e8d2c3538db56", "bootstrap-native/arm64-v8a/_ctypes.cpython-314-aarch64-linux-android.so": "50a6a36dab6a9ebe4fca5797872e8d2c3538db56",
"bootstrap-native/arm64-v8a/mmap.cpython-314-aarch64-linux-android.so": "b571492fdceac1279bedc8163e27ac205e104b14", "bootstrap-native/arm64-v8a/mmap.cpython-314-aarch64-linux-android.so": "b571492fdceac1279bedc8163e27ac205e104b14",
"app.imy": "8e998f6e20dc7152ab075f8edf0644d411cd7f76", "app.imy": "fea5d90dd93b0e7264079d53d95d61e7a6f232c0",
"stdlib-common.imy": "8715635411fe145000fbf3b7ce4c3a085646d300", "stdlib-common.imy": "8715635411fe145000fbf3b7ce4c3a085646d300",
"bootstrap-native/arm64-v8a/_random.cpython-314-aarch64-linux-android.so": "1c6c12f9f43546da0e108031f824b7fd6e79a36a", "bootstrap-native/arm64-v8a/_random.cpython-314-aarch64-linux-android.so": "1c6c12f9f43546da0e108031f824b7fd6e79a36a",
"bootstrap-native/arm64-v8a/_lzma.cpython-314-aarch64-linux-android.so": "6f3f86f607b4d775452c128858fa26d19c7da56a", "bootstrap-native/arm64-v8a/_lzma.cpython-314-aarch64-linux-android.so": "6f3f86f607b4d775452c128858fa26d19c7da56a",
"bootstrap-native/arm64-v8a/zlib.cpython-314-aarch64-linux-android.so": "2d164f648a0f97c1d8710d41cb66754e9daff44e", "bootstrap-native/arm64-v8a/zlib.cpython-314-aarch64-linux-android.so": "2d164f648a0f97c1d8710d41cb66754e9daff44e",
"bootstrap-native/arm64-v8a/binascii.cpython-314-aarch64-linux-android.so": "ff4d2c47c081af6518c8a6a6fa07edcc077b1b6b", "bootstrap-native/arm64-v8a/binascii.cpython-314-aarch64-linux-android.so": "ff4d2c47c081af6518c8a6a6fa07edcc077b1b6b",
"stdlib-arm64-v8a.imy": "ebebf5dc919d7e686936904604fe9a29fede81e9", "stdlib-arm64-v8a.imy": "ebebf5dc919d7e686936904604fe9a29fede81e9",
"requirements-common.imy": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33" "requirements-common.imy": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33"
}, },
"extract_packages": [], "extract_packages": [],
"python_version": "3.14" "python_version": "3.14"
} }
File diff suppressed because it is too large. Load diff
@@ -43,16 +43,41 @@ class UpdateRepository(private val context: Context) {
fun isServerInstalled(): Boolean { fun isServerInstalled(): Boolean {
val ppsrFile = File(context.filesDir, "server/build/build/ppsr.pyc") val ppsrFile = File(context.filesDir, "server/build/build/ppsr.pyc")
return ppsrFile.exists() && ppsrFile.length() > 0 if (!ppsrFile.exists() || ppsrFile.length() < 16) return false
// Check if build code matches current APK
val installedBuildCode = prefs.getInt("installed_build_code", -1)
if (installedBuildCode != com.gacy.android.BuildConfig.VERSION_CODE) {
return false
}
// Validate that python bytecode magic is genuine Python 3.14: [43, 14, 13, 10]
try {
ppsrFile.inputStream().use { stream ->
val magic = ByteArray(4)
if (stream.read(magic) == 4) {
if (magic[0] != 43.toByte() || magic[1] != 14.toByte() || magic[2] != 13.toByte() || magic[3] != 10.toByte()) {
return false // Incompatible bytecode from old install, force refresh!
}
} else {
return false
}
}
} catch (_: Exception) {
return false
}
return true
} }
suspend fun ensureServerInstalled(): Boolean = withContext(Dispatchers.IO) { suspend fun ensureServerInstalled(force: Boolean = false): Boolean = withContext(Dispatchers.IO) {
val ppsrFile = File(context.filesDir, "server/build/build/ppsr.pyc") val ppsrFile = File(context.filesDir, "server/build/build/ppsr.pyc")
if (ppsrFile.exists() && ppsrFile.length() > 0) { if (!force && isServerInstalled()) {
return@withContext true return@withContext true
} }
try { try {
com.gacy.android.CrashTracker.breadcrumb(context, "UpdateRepository: ensureServerInstalled start (force=$force)")
val stagingDir = File(context.filesDir, "server/staging") val stagingDir = File(context.filesDir, "server/staging")
val targetServerDir = File(context.filesDir, "server/build/build") val targetServerDir = File(context.filesDir, "server/build/build")
@@ -60,6 +85,7 @@ class UpdateRepository(private val context: Context) {
stagingDir.mkdirs() stagingDir.mkdirs()
// 1. Extract build.zip from bundled assets // 1. Extract build.zip from bundled assets
com.gacy.android.CrashTracker.breadcrumb(context, "UpdateRepository: extracting build.zip")
extractAssetZip("build.zip", stagingDir) extractAssetZip("build.zip", stagingDir)
val stagedBuildDir = File(stagingDir, "build") val stagedBuildDir = File(stagingDir, "build")
@@ -67,12 +93,25 @@ class UpdateRepository(private val context: Context) {
// 2. Extract runtime packages into effective directory // 2. Extract runtime packages into effective directory
try { try {
com.gacy.android.CrashTracker.breadcrumb(context, "UpdateRepository: extracting packages.zip")
extractAssetZip("packages.zip", effectiveStagedDir) extractAssetZip("packages.zip", effectiveStagedDir)
} catch (e: Exception) { } catch (e: Exception) {
com.gacy.android.CrashTracker.breadcrumb(context, "UpdateRepository: packages.zip notice: ${e.message}")
e.printStackTrace() e.printStackTrace()
} }
// Keep the player's settings when refreshing the bundled server.
for (name in listOf("persistent.json", "config.json")) {
val existing = File(targetServerDir, "misc/$name")
if (existing.isFile) {
val destination = File(effectiveStagedDir, "misc/$name")
destination.parentFile?.mkdirs()
existing.copyTo(destination, overwrite = true)
}
}
// 3. Move/Rename into place // 3. Move/Rename into place
com.gacy.android.CrashTracker.breadcrumb(context, "UpdateRepository: moving to target directory")
if (targetServerDir.exists()) { if (targetServerDir.exists()) {
targetServerDir.deleteRecursively() targetServerDir.deleteRecursively()
} }
@@ -85,9 +124,16 @@ class UpdateRepository(private val context: Context) {
} }
if (stagingDir.exists()) stagingDir.deleteRecursively() if (stagingDir.exists()) stagingDir.deleteRecursively()
setInstalledVersion("4.6.53-embedded") prefs.edit()
.putInt("installed_build_code", com.gacy.android.BuildConfig.VERSION_CODE)
.putString(PREF_INSTALLED_VERSION, "4.6.53-embedded-py314")
.apply()
setInstalledVersion("4.6.53-embedded-py314")
com.gacy.android.CrashTracker.breadcrumb(context, "UpdateRepository: ensureServerInstalled finished success=${ppsrFile.exists()}")
return@withContext ppsrFile.exists() return@withContext ppsrFile.exists()
} catch (e: Exception) { } catch (e: Exception) {
com.gacy.android.CrashTracker.breadcrumb(context, "UpdateRepository: ensureServerInstalled error: ${e.message}")
e.printStackTrace() e.printStackTrace()
return@withContext false return@withContext false
} }
+520 -42
View File
@@ -8,6 +8,12 @@ import socket
import struct import struct
import warnings import warnings
# === SYS.EXIT INTERCEPTOR (Prevent Android Process Termination) ===
def _safe_sys_exit(code=0):
print(f"[Sys] Intercepted sys.exit({code}), prevented Android process exit.")
raise SystemExit(code)
sys.exit = _safe_sys_exit
# === GLOBAL WARNING SUPPRESSION & METADATA SHIM (very early) === # === GLOBAL WARNING SUPPRESSION & METADATA SHIM (very early) ===
try: try:
import importlib.metadata import importlib.metadata
@@ -288,10 +294,14 @@ def _sanitize_persistent_file(persistent_file):
data[k][sub_k] = copy.deepcopy(sub_v) data[k][sub_k] = copy.deepcopy(sub_v)
modified = True modified = True
if "send_hotfix" not in data:
data["send_hotfix"] = True
modified = True
if modified or not os.path.exists(persistent_file): if modified or not os.path.exists(persistent_file):
with open(persistent_file, "w", encoding="utf-8") as f: with open(persistent_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=4) json.dump(data, f, indent=4)
print("[Patch] persistent.json validated and auto-healed with complete avatar/scene defaults") print("[Patch] persistent.json validated and auto-healed with complete avatar/scene defaults (send_hotfix=True)")
except Exception as e: except Exception as e:
print(f"[Patch Warning] sanitize persistent: {e}") print(f"[Patch Warning] sanitize persistent: {e}")
@@ -332,32 +342,7 @@ def kill_server_process():
pass pass
_active_server_instance = None _active_server_instance = None
try: # Do not close unrelated application sockets or interrupt the Android main thread.
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 --- # --- 5. Main Execution Entrypoint ---
def run_server(ppsr_path, activity_context=None): def run_server(ppsr_path, activity_context=None):
@@ -458,13 +443,183 @@ def run_server(ppsr_path, activity_context=None):
except Exception as e: except Exception as e:
print(f"[Patch Warning] Manager persistent patch: {e}") print(f"[Patch Warning] Manager persistent patch: {e}")
# --- 4.6 Command IDs & Router Patch ---
CORE_46 = {
"ChangeLineupLeaderCsReq": 798,
"ChangeLineupLeaderScRsp": 747,
"ClientDownloadDataScNotify": 60,
"DressAvatarSkinCsReq": 357,
"DressAvatarSkinScRsp": 382,
"GetAllLineupDataCsReq": 704,
"GetAllLineupDataScRsp": 794,
"GetAssistListCsReq": 2962,
"GetAssistListScRsp": 2980,
"GetAvatarDataCsReq": 374,
"GetAvatarDataScRsp": 319,
"GetBagCsReq": 574,
"GetBagScRsp": 519,
"GetBasicInfoCsReq": 46,
"GetBasicInfoScRsp": 20,
"GetBigDataAllRecommendCsReq": 2437,
"GetBigDataAllRecommendScRsp": 2411,
"GetCurLineupDataCsReq": 731,
"GetCurLineupDataScRsp": 787,
"GetCurSceneInfoCsReq": 1459,
"GetCurSceneInfoScRsp": 1418,
"GetFriendAssistListCsReq": 2978,
"GetFriendAssistListScRsp": 2939,
"GetFriendListInfoCsReq": 2974,
"GetFriendListInfoScRsp": 2919,
"GetMissionStatusCsReq": 1204,
"GetMissionStatusScRsp": 1294,
"GetRechargeGiftInfoCsReq": 8375,
"GetRechargeGiftInfoScRsp": 8368,
"GetUnreleasedBlockInfoCsReq": 4190,
"GetUnreleasedBlockInfoScRsp": 4112,
"JoinLineupCsReq": 705,
"JoinLineupScRsp": 771,
"PVEBattleResultCsReq": 174,
"PVEBattleResultScRsp": 119,
"PlayerGetTokenCsReq": 5,
"PlayerGetTokenScRsp": 71,
"PlayerHeartBeatCsReq": 45,
"PlayerHeartBeatScRsp": 16,
"PlayerLoginCsReq": 74,
"PlayerLoginFinishCsReq": 9,
"PlayerLoginFinishScRsp": 13,
"PlayerLoginScRsp": 19,
"PlayerSyncScNotify": 674,
"QuitLineupCsReq": 759,
"QuitLineupScRsp": 718,
"ReplaceLineupCsReq": 782,
"ReplaceLineupScRsp": 715,
"SceneCastSkillCsReq": 1405,
"SceneCastSkillScRsp": 1471,
"SceneEntityMoveCsReq": 1474,
"SceneEntityMoveScNotify": 1427,
"SceneEntityMoveScRsp": 1419,
"SceneGroupRefreshScNotify": 1434,
"SetAvatarEnhancedIdCsReq": 381,
"SetAvatarEnhancedIdScRsp": 322,
"StartCocoonStageCsReq": 1444,
"StartCocoonStageScRsp": 1481,
"SyncLineupNotify": 727,
"TakeOffAvatarSkinCsReq": 315,
"TakeOffAvatarSkinScRsp": 323
}
try: try:
import json
import game.cmdid as _cmdid_mod
import game.router as _router_mod import game.router as _router_mod
_orig_router_handle = _router_mod.handle import game.packet as _packet_mod
TABLE_46 = dict(CORE_46)
for json_path in [
os.path.join(ppsr_dir, "cmd_ids_4_6.json"),
os.path.join(misc_dir, "cmd_ids_4_6.json"),
r"/data/data/com.gacy.android/files/server/cmd_ids_4_6.json",
r"/data/data/com.gacy.android/files/server/build/build/cmd_ids_4_6.json"
]:
if os.path.exists(json_path):
try:
with open(json_path, "r", encoding="utf-8") as _cjf:
_raw = json.load(_cjf)
for _k, _v in _raw.items():
_name = _k[3:] if _k.startswith("Cmd") else _k
TABLE_46[_name] = _v
if "GetFriendListInfoScRsp" not in TABLE_46:
TABLE_46["GetFriendListInfoScRsp"] = 2919
print(f"[Patch] Loaded {len(TABLE_46)} 4.6 commands from {json_path}")
break
except Exception as _cje:
print(f"[Patch Warning] Load cmd_ids_4_6.json: {_cje}")
_cmdid_mod.cmd_dict = TABLE_46
_router_mod.cmd_dict = TABLE_46
_router_mod.cmds = list(TABLE_46.keys())
_router_mod.cmd_ids = list(TABLE_46.values())
_handlers_to_wire = [
('GetAvatarDataCsReq', 'GetAvatarDataCsReq'),
('DressAvatarSkinCsReq', 'DressAvatarSkinCsReq'),
('TakeOffAvatarSkinCsReq', 'TakeOffAvatarSkinCsReq'),
('SetAvatarEnhancedIdCsReq', 'SetAvatarEnhancedIdCsReq'),
('PVEBattleResultCsReq', 'PVEBattleResultCsReq'),
('GetBagCsReq', 'GetBagCsReq'),
('GetCurLineupDataCsReq', 'GetCurLineupDataCsReq'),
('JoinLineupCsReq', 'JoinLineupCsReq'),
('QuitLineupCsReq', 'QuitLineupCsReq'),
('ChangeLineupLeaderCsReq', 'ChangeLineupLeaderCsReq'),
('GetAllLineupDataCsReq', 'GetAllLineupDataCsReq'),
('ReplaceLineupCsReq', 'ReplaceLineupCsReq'),
('GetUnreleasedBlockInfoCsReq', 'GetUnreleasedBlockInfoCsReq'),
('GetMissionStatusCsReq', 'GetMissionStatusCsReq'),
('PlayerLoginCsReq', 'PlayerLoginCsReq'),
('PlayerGetTokenCsReq', 'PlayerGetTokenCsReq'),
('GetBasicInfoCsReq', 'GetBasicInfoCsReq'),
('PlayerHeartBeatCsReq', 'PlayerHeartBeatCsReq'),
('PlayerLoginFinishCsReq', 'PlayerLoginFinishCsReq'),
('GetRechargeGiftInfoCsReq', 'GetRechargeGiftInfoCsReq'),
('GetBigDataAllRecommendCsReq', 'GetBigDataAllRecommendCsReq'),
('SceneCastSkillCsReq', 'SceneCastSkillCsReq'),
('GetCurSceneInfoCsReq', 'GetCurSceneInfoCsReq'),
('StartCocoonStageCsReq', 'StartCocoonStageCsReq'),
('SceneEntityMoveCsReq', 'SceneEntityMoveCsReq'),
]
_new_handle_funct = {}
for req_name, handler_name in _handlers_to_wire:
if hasattr(_router_mod, handler_name):
h_mod = getattr(_router_mod, handler_name)
if hasattr(h_mod, 'handle'):
cid = TABLE_46.get(req_name)
if cid is not None:
_new_handle_funct[cid] = h_mod.handle
_router_mod.handle_funct = _new_handle_funct
def _safe_to_rsp(req_name):
if req_name.endswith('CsReq'):
rsp_name = req_name[:-5] + 'ScRsp'
if rsp_name in TABLE_46:
return TABLE_46[rsp_name]
return -1
_router_mod.to_rsp = _safe_to_rsp
def _safe_router_handle(data): def _safe_router_handle(data):
try: try:
return _orig_router_handle(data) cmd, head, body = _packet_mod.read_packet(data)
if cmd not in _router_mod.cmd_ids:
print(f"[Packet Warning] Unhandled cmd_id {cmd}")
return b''
cmd_name = _router_mod.cmds[_router_mod.cmd_ids.index(cmd)]
rsp_cmd = _router_mod.to_rsp(cmd_name)
if cmd not in _router_mod.handle_funct:
if rsp_cmd != -1:
print(f"[Packet] Recv dummy {cmd_name} ({cmd}) -> Send dummy {rsp_cmd}")
return _packet_mod.write_packet(rsp_cmd, b'', b'')
else:
print(f"[Packet] Recv notify/unknown {cmd_name} ({cmd})")
return b''
handler_funct = _router_mod.handle_funct[cmd]
rsp_body = handler_funct(body)
if cmd == TABLE_46.get('StartCocoonStageCsReq'):
try:
import game.services.manager as _mgr
_mgr.Manager.load_persistent()
_mgr.Manager.load_freesr()
except Exception:
pass
print(f"[Packet] Recv {cmd_name} ({cmd}) -> Send {rsp_cmd}")
return _packet_mod.write_packet(rsp_cmd, b'', rsp_body)
except Exception as e: except Exception as e:
import traceback import traceback
print(f"[Router Warning] Handled exception in router.handle: {e}") print(f"[Router Warning] Handled exception in router.handle: {e}")
@@ -472,24 +627,347 @@ def run_server(ppsr_path, activity_context=None):
return b'' return b''
_router_mod.handle = _safe_router_handle _router_mod.handle = _safe_router_handle
print("[Patch] game.router.handle wrapped with crash-guard") print(f"[Patch] game.router mapped with {len(_new_handle_funct)} 4.6 handlers and crash-guard")
except Exception as e: except Exception as e:
print(f"[Patch Warning] Router crash-guard: {e}") print(f"[Patch Warning] 4.6 router patch: {e}")
print(f"Loading bytecode target: {ppsr_path}...")
with open(ppsr_path, "rb") as f:
code_bytes = f.read()
# --- SafeHotfixDict & Dispatch Patch (Prevent 1001_3 KeyError & reachable Gateway IP) ---
try: try:
import marshal import json
code_obj = marshal.loads(code_bytes[16:]) import base64
except Exception: import dispatch.dispatchserver as _ds_mod
code_obj = compile(code_bytes, ppsr_path, "exec")
class SafeHotfixDict(dict):
def __getitem__(self, key):
if key in self:
return super().__getitem__(key)
base = str(key).split('_')[0]
if base in self:
print(f"[SafeHotfix] Version resolved: {key} -> {base}")
return super().__getitem__(base)
for cand in ['OSBETAAndroid4.6.54', 'OSBETAAndroid4.6.53', 'OSBETAWin4.6.54', 'OSBETAWin4.6.53', 'OSBETAAndroid4.6.51', 'OSBETAWin4.6.51']:
if cand in self:
print(f"[SafeHotfix] Version fallback: {key} -> {cand}")
return super().__getitem__(cand)
if len(self) > 0:
first_val = next(iter(self.values()))
print(f"[SafeHotfix] Unknown version {key}, falling back to default hotfix.")
return first_val
return {}
if hasattr(_ds_mod, 'hotfix') and isinstance(_ds_mod.hotfix, dict):
_ds_mod.hotfix = SafeHotfixDict(_ds_mod.hotfix)
elif os.path.exists(os.path.join(misc_dir, "hotfix.json")):
with open(os.path.join(misc_dir, "hotfix.json"), "r", encoding="utf-8") as _hf_file:
_ds_mod.hotfix = SafeHotfixDict(json.load(_hf_file))
if hasattr(_ds_mod, 'check_hotfix'):
_orig_check_hotfix = _ds_mod.check_hotfix
def _safe_check_hotfix(version, full_path):
try:
return _orig_check_hotfix(version, full_path)
except Exception as ex:
print(f"[SafeHotfix Warning] check_hotfix handled safely: {ex}")
_ds_mod.check_hotfix = _safe_check_hotfix
# Keep _ds_mod.HOST = "127.0.0.1" for internal references
_ds_mod.HOST = "127.0.0.1"
def resolve_client_gate_ip():
try:
from flask import request
if request.remote_addr in ('127.0.0.1', '::1', 'localhost'):
return '127.0.0.1'
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
probe.connect((request.remote_addr, 9))
return probe.getsockname()[0]
except Exception:
pass
return '127.0.0.1'
def _patched_query_gateway():
from flask import request
version = request.args.get('version')
if version is None or request.method == 'HEAD':
return '', 200
from msg import classes_pb2
gate_ip = resolve_client_gate_ip()
print(f"[Gateway] Resolved gateserver target: {gate_ip}:{_ds_mod.GAME_PORT} for client {request.remote_addr}")
response = classes_pb2.Gateserver(
ip=gate_ip,
port=_ds_mod.GAME_PORT,
unk1=True, unk2=True, unk3=True, unk4=True, unk5=True,
unk6=True, unk7=True, unk8=True
)
# Check if persistent specifies send_hotfix (Default to True for 4.6)
should_send_hotfix = True
try:
import game.services.manager as _mgr_mod
if hasattr(_mgr_mod, 'Manager') and hasattr(_mgr_mod.Manager, 'persistent'):
should_send_hotfix = bool(_mgr_mod.Manager.persistent.get('send_hotfix', True))
except Exception:
should_send_hotfix = True
if should_send_hotfix:
clean_ver = version.split('_')[0] if version else version
hf = _ds_mod.hotfix.get(version) or _ds_mod.hotfix.get(clean_ver)
if isinstance(hf, dict) and hf:
response.asset_bundle_url = str(hf.get('asset_bundle_url', ''))
response.asset_bundle_url_b = str(hf.get('asset_bundle_url_b', ''))
response.ex_resource_url = str(hf.get('ex_resource_url', ''))
response.lua_url = str(hf.get('lua_url', ''))
response.ifix_url = str(hf.get('ifix_url', ''))
response.lua_version = str(hf.get('lua_version', '0'))
response.ifix_version = str(hf.get('ifix_version', '0'))
print(f"[Gateway] Hotfix populated for {version} (asset_bundle_url={response.asset_bundle_url[:35]}..., lua_ver={response.lua_version})")
else:
print(f"[Gateway Warning] No hotfix metadata found for {version}")
return 'Unknown client version', 400
else:
print(f"[Gateway] Hotfix download bypassed (send_hotfix=False) for client {request.remote_addr}")
return base64.b64encode(response.SerializeToString())
def _patched_query_dispatch():
from flask import request
from msg import classes_pb2
gate_ip = resolve_client_gate_ip()
print(f"[Dispatch] Query dispatch from {request.remote_addr}, offering gateway at {gate_ip}:{_ds_mod.PORT}")
response = classes_pb2.Dispatch(
msg='OK',
retcode=0,
top_server_region_name='pp_sr',
region_list=[
classes_pb2.RegionInfo(
name='ppSR',
title='ppSR',
dispatch_url=f'http://{gate_ip}:{_ds_mod.PORT}/query_gateway',
env_type='9',
display_name='ppSR'
)
]
)
return base64.b64encode(response.SerializeToString())
# Patch DispatchServer.run_server to bind 0.0.0.0
if hasattr(_ds_mod, 'DispatchServer') and hasattr(_ds_mod.DispatchServer, 'run_server'):
@classmethod
def _patched_run_server(cls):
try:
import logging, flask
logging.getLogger('werkzeug').disabled = True
flask.cli.show_server_banner = lambda *args: None
print(f"Dispatch server listening on 0.0.0.0:{_ds_mod.PORT}")
print(f"Forwarding to 127.0.0.1:{_ds_mod.GAME_PORT}")
_ds_mod.app.run(host="0.0.0.0", port=_ds_mod.PORT, threaded=False, use_reloader=False, debug=False)
except Exception as ex:
print(f"[DispatchServer Error]: {ex}")
_ds_mod.DispatchServer.run_server = _patched_run_server
_app = _ds_mod.app
_app.view_functions['query_gateway'] = _patched_query_gateway
_ds_mod.query_gateway = _patched_query_gateway
_app.view_functions['query_dispatch'] = _patched_query_dispatch
_ds_mod.query_dispatch = _patched_query_dispatch
@_app.before_request
def _log_http_req():
try:
from flask import request as _req
if any(k in _req.path for k in ['log', 'Upload', 'report']):
body_preview = _req.get_data()[:300].decode('utf-8', errors='ignore')
print(f"[HTTP Telemetry] {_req.method} {_req.path}: {body_preview}")
else:
print(f"[HTTP] {_req.method} {_req.path}")
except Exception:
pass
@_app.after_request
def _log_http_rsp(resp):
try:
from flask import request as _req
print(f"[HTTP] {_req.method} {_req.path} -> {resp.status_code}")
resp.headers['Access-Control-Allow-Origin'] = '*'
resp.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS, HEAD'
resp.headers['Access-Control-Allow-Headers'] = '*'
except Exception:
pass
return resp
def _handle_login_rsp(*args, **kwargs):
try:
return _ds_mod.login_by_password(), 200, {'Content-Type': 'application/json'}
except Exception as e:
print(f"[Login Error]: {e}")
return '{"retcode":0,"message":"OK","data":{}}', 200, {'Content-Type': 'application/json'}
def _handle_load_config(*args, **kwargs):
return '{"retcode":0,"message":"OK","data":{"client_custom_config":"","client_saved_config":""}}', 200, {'Content-Type': 'application/json'}
for _route in [
'/<region>/account/ma-passport/api/appLoginByPassword',
'/account/ma-passport/api/appLoginByPassword',
'/<region>/account/ma-cn-passport/app/loginByPassword',
'/account/ma-cn-passport/app/loginByPassword',
'/<region>/account/ma-passport/token/verify',
'/account/ma-passport/token/verify',
'/<region>/account/auth/api/login',
'/account/auth/api/login'
]:
try:
_app.add_url_rule(_route, endpoint=f'flex_{_route}', view_func=_handle_login_rsp, methods=['OPTIONS', 'POST', 'GET'])
except Exception:
pass
for _cfg_route in [
'/<region>/mdk/shield/api/loadConfig',
'/mdk/shield/api/loadConfig',
'/hkrpg_<region>/mdk/shield/api/loadConfig',
'/hkrpg_global/mdk/shield/api/loadConfig'
]:
try:
_app.add_url_rule(_cfg_route, endpoint=f'cfg_{_cfg_route}', view_func=_handle_load_config, methods=['OPTIONS', 'POST', 'GET'])
except Exception:
pass
@_app.errorhandler(405)
def _handle_405(e):
return '{"retcode":0,"message":"OK","data":{}}', 200, {'Content-Type': 'application/json'}
@_app.errorhandler(404)
def _handle_404(e):
try:
from flask import request as _req
print(f"[HTTP 404 Fallback] {_req.method} {_req.path}")
if any(k in _req.path for k in ['login', 'passport', 'token', 'auth', 'shield']):
return _ds_mod.login_by_password(), 200, {'Content-Type': 'application/json'}
if 'combo' in _req.path:
return _ds_mod.login_v2(), 200, {'Content-Type': 'application/json'}
if 'risky' in _req.path:
return _ds_mod.risky_check(), 200, {'Content-Type': 'application/json'}
except Exception:
pass
return '{"retcode":0,"message":"OK","data":{}}', 200, {'Content-Type': 'application/json'}
print("[Patch] dispatchserver patched with reachable Gateserver IP, 4.6 protocol, and flexible routes")
except Exception as e:
print(f"[Patch Warning] Dispatch server patch: {e}")
# --- GameServer address & protected KCP loop patch ---
try:
import game.gameserver as _gs_mod
if hasattr(_gs_mod, 'GameServer'):
_gs_mod.GameServer.address = "0.0.0.0"
@classmethod
def _patched_read_loop(cls):
print(f"[GameServer] UDP KCP Listener active on {cls.address}:{cls.port}")
while cls.running:
try:
message, address = cls.server_socket.recvfrom(_gs_mod.IKCP_MTU_DEF)
if not message:
continue
if len(message) == 20:
print(f"[GameServer] Received Handshake ({len(message)} bytes) from {address}")
cls.handle_handshake(address, message)
continue
if not hasattr(cls, 'kcp') or cls.kcp is None:
print(f"[GameServer Warning] Received {len(message)} bytes from {address} before KCP established, ignoring.")
continue
cls.kcp.input(message)
cls.kcp.flush()
cls.kcp.update(int(time.time() * 1000))
packet = cls.kcp.recv()
while len(packet) > 0 and cls.running:
rsp = _router_mod.handle(packet)
if len(rsp) != 0:
cls.send(rsp)
packet = cls.kcp.recv()
except Exception as ex:
if cls.running:
print(f"[GameServer Exception in read_loop]: {ex}")
time.sleep(0.01)
_gs_mod.GameServer.read_loop = _patched_read_loop
@classmethod
def _patched_handle_handshake(cls, address, data):
try:
from game.handshakeutils import HandShake, HandshakeType
handshake = HandShake.parse(data)
print(f"[GameServer] Parsed handshake type={handshake} from {address}")
if handshake == HandshakeType.CONNECT:
cls.establish_kcp()
rsp_handshake = HandShake.build_rsp_handshake(cls.conv_id)
cls.client_socket = address
cls.send_udp(rsp_handshake)
print(f"[GameServer] Handshake accepted! Sent response to {address}, conv_id={cls.conv_id}")
except Exception as ex:
print(f"[GameServer Handshake Error]: {ex}")
_gs_mod.GameServer.handle_handshake = _patched_handle_handshake
@classmethod
def _patched_send_udp(cls, data):
if cls.client_socket is not None:
try:
cls.server_socket.sendto(data, cls.client_socket)
except Exception as ex:
print(f"[GameServer send_udp Error]: {ex}")
_gs_mod.GameServer.send_udp = _patched_send_udp
_orig_gs_start = _gs_mod.GameServer.start
@classmethod
def _safe_gs_start(cls):
cls.address = "0.0.0.0"
try:
sock = getattr(cls, 'server_socket', None)
if sock is None or getattr(sock, 'fileno', lambda: -1)() == -1:
cls.server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cls.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
_orig_gs_start()
print(f"[GameServer] Bound UDP socket on {cls.address}:{cls.port}")
except Exception as ex:
print(f"[GameServer Warning] start error: {ex}")
_gs_mod.GameServer.start = _safe_gs_start
print("[Patch] GameServer address patched to 0.0.0.0 with protected read_loop and handshake logging")
except Exception as e:
print(f"[Patch Warning] GameServer patch: {e}")
ppsr_py = os.path.join(ppsr_dir, "ppsr.py")
if os.path.exists(ppsr_py):
print(f"Loading source script target: {ppsr_py}...")
with open(ppsr_py, "r", encoding="utf-8") as f:
code_str = f.read()
code_obj = compile(code_str, ppsr_py, "exec")
target_file = ppsr_py
else:
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")
target_file = ppsr_path
main_globals = { main_globals = {
"__name__": "__main__", "__name__": "__main__",
"__file__": ppsr_path, "__file__": target_file,
"__doc__": None, "__doc__": None,
"sys": sys, "sys": sys,
"os": os, "os": os,
@@ -498,7 +976,7 @@ def run_server(ppsr_path, activity_context=None):
try: try:
exec(code_obj, main_globals) exec(code_obj, main_globals)
except KeyboardInterrupt: except (KeyboardInterrupt, SystemExit):
print("[System]: Server loop interrupted cleanly.") print("[System]: Server loop interrupted cleanly.")
except Exception as e: except Exception as e:
print(f"[System]: Server terminated: {e}") print(f"[System]: Server terminated: {e}")
+200
View File
@@ -0,0 +1,200 @@
import sys
import os
import builtins
import time
import _thread
import gc
import socket
import struct
import warnings
# === GLOBAL WARNING SUPPRESSION (very early) ===
try:
from marshmallow.warnings import RemovedInMarshmallow4Warning
warnings.filterwarnings("ignore", category=RemovedInMarshmallow4Warning)
warnings.filterwarnings("ignore", message=".*default.*deprecated.*")
warnings.filterwarnings("ignore", message=".*metadata.*deprecated.*")
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 = ""
def write(self, message):
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
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")
# --- 4. Shutdown Logic ---
def kill_server_process():
global _active_server_instance
print("[Shutdown]: Force-stopping server execution...")
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")
if os.path.exists(ppsr_dir):
os.chdir(ppsr_dir)
print(f"Working directory locked: {os.getcwd()}")
for path in [ppsr_dir, misc_dir]:
if os.path.exists(path) and path not in sys.path:
sys.path.insert(0, path)
# --- Werkzeug Patch ---
try:
import werkzeug.serving
_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:
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}")
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}")