# revival-channel.py - one of the three game scripts the Super Mecha Crows Android kit adds. # The installer puts it into the game's update file (patch_npk/p_script.npk on the phone) under the game's own name # common.platform.channel, next to the game's original module, which it copied out of YOUR phone's game under # common.platform.channel_original. This script loads that original unchanged, then runs the Crows part below inside it. import sys import common.platform.channel_original as _revival_original sys.modules['common.platform.channel'] = _revival_original _REVIVAL_PART = '# ---- [REVIVAL] Steam-free login (docs/STEAMFREE.md) -- appended by tools/override/build_override.py after the\n# original common/platform/channel.py body (class Channel, LOGIN_CODE, PROTOCOL_CODE_ACCEPT, game3d, json exist).\n#\n# With launcher credentials present (env SMC_LOGIN_JSON, else /smc_login.json =\n# {"username": ..., "token": ..., "udid": ...}) Channel.init() is wrapped: after the original init the native UniSDK\n# object in self._channel is replaced by _RevivalSdkChannel and is_valid is forced True, so the client logs in as\n# \'@smc.win.163.com\' without Steam and sends the password token as `sessionid` in channel_login\'s sauth dict.\n# Without credentials NOTHING changes (the Steam path stays as it is), so the same archive fits both game copies.\n# NO bare print() anywhere (stdout is C_debug.logoutput without flush(): print() aborts the process); logging goes\n# through the game\'s LogManager only, and every step is guarded so nothing can propagate into the import.\nimport os as _r_os\nimport json as _r_json\n\n_REVIVAL_CHANNEL_MARKER = \'[REVIVAL] Steam-free login override present\' # build_override.py checks this literal\n\ntry:\n from mobile.mobilelog.LogManager import LogManager as _r_LogManager\n _r_log = _r_LogManager.get_logger(\'channel\')\nexcept Exception:\n _r_log = None\n\n\ndef _r_info(msg):\n try:\n if _r_log is not None:\n _r_log.info(\'[REVIVAL] %s\' % (msg,))\n except Exception:\n pass\n\n\ndef _r_load_credentials():\n """The launcher\'s account: {\'username\', \'token\', \'udid\'} or None."""\n raw = None\n try:\n raw = _r_os.environ.get(\'SMC_LOGIN_JSON\')\n except Exception:\n raw = None\n if not raw:\n try:\n path = _r_os.path.join(game3d.get_doc_dir(), \'smc_login.json\')\n if _r_os.path.exists(path):\n with open(path, \'r\') as fh:\n raw = fh.read()\n except Exception as exc:\n _r_info(\'smc_login.json unreadable: %r\' % (exc,))\n raw = None\n if not raw:\n return None\n try:\n d = _r_json.loads(raw)\n user = str(d.get(\'username\') or \'\').strip()\n token = str(d.get(\'token\') or \'\').strip()\n if not user or not token:\n _r_info(\'smc_login.json without username/token: ignored\')\n return None\n return {\'username\': user, \'token\': token, \'udid\': str(d.get(\'udid\') or (\'smc-\' + user)).strip()}\n except Exception as exc:\n _r_info(\'smc_login.json unparsable: %r\' % (exc,))\n return None\n\n\nclass _RevivalSdkChannel(object):\n """Stands in for the native UniSDK channel object (social.get_channel()). Everything Channel reads from it is\n a property or a no-op; login() and show_compact_view() answer through the callbacks Channel.init_event installs\n (deferred by game3d.delay_exec so the order of events matches the real SDK\'s asynchronous replies)."""\n name = \'smc\'\n platform = \'pc\'\n distribution_channel = \'smc\'\n sdk_version = \'1.5.4\'\n is_init = True\n _ABSENT = (\'switch_account\',) # hasattr() probes that must stay False (has_standalone_sw_user)\n\n def __init__(self, creds, owner=None):\n self.udid = creds[\'udid\']\n self._owner = owner # the Channel wrapper: its bound callbacks are called directly when\n # Channel.init_event has not (yet) wired them onto this object\n sauth = {\'gameid\': \'g93naxx2gb\', \'login_channel\': \'smc\', \'app_channel\': \'smc\', \'platform\': \'pc\',\n \'sdkuid\': creds[\'username\'], \'sessionid\': creds[\'token\'], \'sdk_version\': \'1.5.4\',\n \'udid\': self.udid, \'deviceid\': self.udid, \'aim_info\': \'\', \'client_login_sn\': \'\', \'gas_token\': \'\',\n \'source_platform\': \'pc\', \'ip\': \'127.0.0.1\'}\n self._props = {\'APPID\': \'smc-revival\', \'UID\': creds[\'username\'], \'ORIGIN_GUEST_UID\': \'\',\n \'LOGIN_CHANNEL\': \'smc\', \'NT_PLATFORM\': \'\', \'SAUTH_JSON\': _r_json.dumps(sauth),\n \'USERINFO_AID\': \'\', \'SESSION\': creds[\'token\'], \'TIMESTAMP\': \'\', \'EXTRA_DATA\': \'\',\n \'HW_REALNAME\': \'\'}\n self.login_callback = None\n self.logout_callback = None\n self.protocol_finish_callback = None\n self.extend_callback = None\n self.order_callback = None\n self.query_product_callback = None\n self.exit_app_callback = None\n self.is_daren_updated_callback = None\n\n def __str__(self):\n return \'\' % (self._props.get(\'UID\'),)\n\n __repr__ = __str__\n\n # ---- props ----\n def get_prop_str(self, prop):\n v = self._props.get(prop, \'\')\n return v if isinstance(v, str) else str(v)\n\n def get_prop_int(self, prop, default=0):\n v = self._props.get(prop)\n return v if isinstance(v, int) else default\n\n def set_prop_str(self, prop, val):\n self._props[prop] = val\n\n def set_prop_int(self, prop, val):\n self._props[prop] = val\n\n def has_prop(self, prop):\n return prop in self._props\n\n def has_feature(self, feature):\n return False\n\n def get_auth_type(self):\n return 0\n\n # ---- the login conversation ----\n def _cb(self, name):\n """The Channel wrapper\'s callback: wired onto this object by Channel.init_event, else taken from the\n wrapper itself (init_event may run before init, or not at all for a channel that was invalid at init)."""\n fn = getattr(self, name, None)\n if fn is None and self._owner is not None:\n fn = getattr(self._owner, name, None)\n _r_info(\'%s not wired on the SDK object; using the one of the Channel wrapper\' % (name,))\n return fn\n\n def _later(self, label, fn, *args):\n def _fire():\n _r_info(\'deferred %s firing\' % (label,))\n try:\n fn(*args)\n except Exception:\n import traceback\n _r_info(\'%s raised: %s\' % (label, traceback.format_exc()))\n try:\n r = game3d.delay_exec(100, _fire)\n _r_info(\'%s scheduled via game3d.delay_exec(100) -> %r\' % (label, r))\n except Exception as exc:\n _r_info(\'game3d.delay_exec failed (%r): calling %s at once\' % (exc, label))\n _fire()\n\n def login(self):\n _r_info(\'fake SDK login as %s\' % (self._props.get(\'UID\'),))\n cb = self._cb(\'login_callback\')\n if cb is None:\n _r_info(\'no login_callback anywhere: cannot finish the login\')\n return\n self._later(\'login_callback(OK)\', cb, LOGIN_CODE.OK)\n\n def logout(self):\n _r_info(\'fake SDK logout\')\n cb = self._cb(\'logout_callback\')\n if cb is not None:\n self._later(\'logout_callback\', cb, 0)\n\n def show_compact_view(self, confirm_only=False):\n cb = self.protocol_finish_callback or (getattr(self._owner, \'on_protocol_finish_callback\', None) if self._owner else None)\n if cb is not None:\n self._later(\'protocol_finish_callback(ACCEPT)\', cb, PROTOCOL_CODE_ACCEPT)\n\n def try_exit(self):\n return False\n\n def extend_func(self, s):\n _r_info(\'SDK extend_func ignored: %s\' % (str(s)[:160],))\n\n _FALSE = (\'is_downloader_enable\', \'is_support_realname_dialog\', \'has_feature\', \'is_daren_updated\')\n _seen = set()\n\n def __getattr__(self, name):\n # Unknown names: the client reads some as DATA (e.g. LobbyMallData.get_charge_info did .split() on one and\n # crashed the lobby load, live 2026-09-23) and calls others as METHODS. _Unknown is an empty str that is\n # also callable, so both shapes work: \'\' for text, a no-op returning None/False for calls.\n if name.startswith(\'__\') or name in self._ABSENT:\n raise AttributeError(name)\n if name not in _RevivalSdkChannel._seen: # the client polls some of these every frame\n _RevivalSdkChannel._seen.add(name)\n _r_info(\'SDK name unknown to the fake (answered as empty/no-op; further uses not logged): %s\' % (name,))\n return _Unknown(name, False if name in self._FALSE else None)\n\n\nclass _Unknown(str):\n """\'\' that can also be called (returns its preset result)."""\n def __new__(cls, name, result=None):\n obj = str.__new__(cls, \'\')\n obj._name, obj._result = name, result\n return obj\n\n def __call__(self, *args, **kwargs):\n return self._result\n\n def __bool__(self):\n return False\n\n\ntry:\n _r_info(_REVIVAL_CHANNEL_MARKER)\n _r_creds = _r_load_credentials()\n if _r_creds:\n _r_orig_init = Channel.init\n\n def _revival_init(self):\n _r_orig_init(self)\n try:\n self._channel = _RevivalSdkChannel(_r_creds, owner=self)\n self.is_valid = True\n _r_info(\'fake SDK channel installed: login name %s@smc.win.163.com\' % (_r_creds[\'username\'],))\n except Exception as exc:\n _r_info(\'fake SDK channel FAILED (Steam path kept): %r\' % (exc,))\n Channel.init = _revival_init\n # LoginHelper.check_svr_data_valid rejects every server row whose svr_platform is \'steam\' unless\n # channel.is_steam_channel() - and then refetches serverlist.txt without end (live 2026-09-23 09:33). The\n # launcher client IS the Steam build minus Steam, so it answers yes; the login name still comes from the fake.\n Channel.is_steam_channel = lambda self: True\n _r_info(\'Steam-free login armed for %s\' % (_r_creds[\'username\'],))\n else:\n _r_info(\'no launcher credentials: stock SDK channel path\')\nexcept Exception as _r_exc:\n _r_info(\'arming failed: %r\' % (_r_exc,))\n\n\n# ---- [REVIVAL] Android: appended to tools/override/channel_post.py for the Android kit (build_android_kit.py) ----\n# (Kept although R161 serves `all` rows: harmless once deployed, needed while prod still lists `steam` rows.)\n# LoginHelper.check_svr_data_valid drops every serverlist row whose svr_platform is \'steam\' unless the client\'s\n# platform is in PLATFORM_LIMIT_DICT[\'steam\'] (WIN32 only). Our server only lists \'steam\' rows, so Android saw no\n# server at all. The check calls channel.get_prop_str(\'JUDGE_PACKAGE\') right before that lookup, so LoginHelper is\n# loaded by then: add PLATFORM_ANDROID to the \'steam\' tuple there, once. Only on Android; PC is untouched.\ntry:\n import sys as _r_sys\n import game3d as _r_g3d\n if _r_g3d.get_platform() == _r_g3d.PLATFORM_ANDROID:\n _r_orig_get_prop_str = Channel.get_prop_str\n _r_android_done = [False]\n\n def _r_android_get_prop_str(self, *args, **kwargs):\n if not _r_android_done[0]:\n try:\n _r_lh = _r_sys.modules.get(\'logic.comsys.login.LoginHelper\')\n if _r_lh is not None:\n _r_lim = _r_lh.PLATFORM_LIMIT_DICT\n if _r_g3d.PLATFORM_ANDROID not in _r_lim[\'steam\']:\n _r_lim[\'steam\'] = tuple(_r_lim[\'steam\']) + (_r_g3d.PLATFORM_ANDROID,)\n _r_android_done[0] = True\n _r_info(\'android: steam server rows allowed\')\n except Exception as _r_exc3:\n _r_android_done[0] = True\n _r_info(\'android platform patch failed: %r\' % (_r_exc3,))\n return _r_orig_get_prop_str(self, *args, **kwargs)\n Channel.get_prop_str = _r_android_get_prop_str\n _r_info(\'android: server platform patch armed\')\n\n # common.http.convert_url re-resolves IP-literal URLs (NetEase\'s iOS NAT64 helper) with AI_DEFAULT /\n # AI_ADDRCONFIG; Android\'s getaddrinfo answers "Invalid value for ai_flags" and the gaierror killed the lobby\n # scene (LobbyMallData.request_charge_info on our shop hotfix\'s http://127.0.0.1:8000/gas3/ URL, 2026-10-01).\n # On failure keep the URL as it is - what the PC client effectively does.\n try:\n import common.http as _r_http\n _r_orig_convert_url = _r_http.convert_url\n\n def _r_safe_convert_url(url, *args, **kwargs):\n try:\n return _r_orig_convert_url(url, *args, **kwargs)\n except Exception as _r_exc4:\n _r_info(\'android: convert_url kept %r (%r)\' % (url, _r_exc4))\n return url\n _r_http.convert_url = _r_safe_convert_url\n _r_info(\'android: convert_url guard armed\')\n except Exception as _r_exc5:\n _r_info(\'android: convert_url guard failed: %r\' % (_r_exc5,))\nexcept Exception as _r_exc2:\n _r_info(\'android arming failed: %r\' % (_r_exc2,))\n' exec(compile(_REVIVAL_PART, 'revival-channel', 'exec'), vars(_revival_original)) globals().update({_k: _v for _k, _v in vars(_revival_original).items() if not _k.startswith('__')})