# revival-ConnectHelper.py # ======================== # This is the ONE game script the Super Mecha Crows kit adds, shown here in full. # # The installer puts it into the game's own update file (package\patch_npk\p_script.npk) under the # name logic\gutils\ConnectHelper: the part of the game that sets up the connection to a server. # revival-ConnectHelper.nxs, next to this file, is this same script compiled into the game's format. # # What it does, in order: # 1. It loads the game's original ConnectHelper, unchanged. The installer copied that out of YOUR OWN # game (package\script.npk) into the update file under the name ConnectHelper_original. # That copy comes from your PC; it is not part of this kit. # 2. It replaces one value: the public key the game uses to recognise its server. NetEase's servers # are gone, so without this the game would refuse every server. # 3. It writes one line to package\log.txt, so you can see that it is active. # 4. It asks the Crows server, once at every start, for the current version of part 5, checks that the # answer really comes from the Crows server, and keeps it in package\Documents\crows\. # 5. It adds this kit's number to the version the game reports when it logs in, and tells the server # how the game was started (through Steam or not; with the game's own Steam file or another one). # The server only accepts Steam logins from the current kit. When yours is too old, the game says # the client version is outdated: download the kit again and run Install-SMC.cmd again. # # That is all it does. Apart from that one request in part 4 (nothing about you is sent), it sends nothing # anywhere, writes nothing else, and changes nothing else in the game. import sys # ---- 1. The game's original ConnectHelper, unchanged ------------------------------------------------ import logic.gutils.ConnectHelper_original as original # From here on, anything in the game that asks for ConnectHelper gets that original module. sys.modules['logic.gutils.ConnectHelper'] = original # The same names are copied here too, for any code that already holds on to this module. globals().update({name: value for name, value in vars(original).items() if not name.startswith('__')}) # ---- 2. The Crows server's public key ------------------------------------------------------------------ # A public key can only VERIFY the server. It cannot unlock or read anything on your PC. REVIVAL_SERVER_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxiQP7dp86S5kBjMhq5UUXEDclfLikyoNcWU4WK63xUNj4lZi0buzsYnuM7w+7g1JN5bnY54/CkPXzrQByaMoOa2RS81vdUQDPmd2HqPEd5c7qoRpd8yrYCiaBE1bEe0cPwLZeVGE9TxIW3/1FtxJiHYNxeqpdoIUPJSijlsGJMwChFhT+AOFvHFaZBgq4bfDR6Dp88c6JtU5b8Y5zzICp6ARxYsSaYuyWpWEtc+iGEfNi5k1gMWCqRsDm1xZw9zeRAnNnkCuyrQ5SWqJ8AVsdezAho56c90Z0E5GpK2L5iLhv37o6xFXAIEPeptMSnyJr9xoQ9GNMjrgdFDruI5mv' try: original.ConnectHelper.GATE_CLIENT_CONFIG['loginkeycontent'] = REVIVAL_SERVER_KEY # lobby connection original.ConnectHelper.BATTLE_CLIENT_CONFIG['key_content'] = REVIVAL_SERVER_KEY # match connection except Exception: pass # ---- 3. One line in package\log.txt ----------------------------------------------------------------------- # (The game has no working print(), so it uses its own logger.) try: original.LogManager.get_logger('ConnectHelper').info('[REVIVAL] ConnectHelper override active, gate key swapped') except Exception: pass LogManager = original.LogManager # ---- 4. The current version of part 5, from the Crows server ------------------------------------------------ # Once at every start this asks the Crows server for the current version of part 5 (two small files), accepts it # only when it is signed with the Crows key below and matches its description, keeps it in # package\Documents\crows\, and runs it in place of the copy written further down. Nothing about you is sent. CROWS_UPDATE_URL = 'https://g93.update.netease.com/crows/' CROWS_UPDATE_KEY_N = 'd3a1ef2cf446ae0dbb18468aed0201042b1a970e404f99ab6df2984063f6fcf36f6c4043c44d4f4198230b00f7d41a10f81a5a43ff8e002eb0283ad85ca18dbf81fb03c29f90547a3636fc4c770f4150caa961ac10f40fc2284c6901b0f1b90aaa9dd492201c884a093ac800ee49d914a640c75da9935f98a812bdb1d691fa7a650732a9ec9b5cdfa31f4b3823232cc61d7d0a54dcd6eb0b406292d0fb7df08df6eae6c4d39a82c92fc2184816ba31f79aa8babb869e8503447d3ba8c3381739cf87d11985b5f8b0fdfc387033a74826fab64693b4d70309b7865566258be77b691925c14b4da36e9b87fec73d4bb5b5431598e94f41e4ae2716bc7a8ff36713' CROWS_UPDATE_KEY_E = 65537 def _crows_update(): """True when a delivered copy of part 5 ran. Never raises: any problem means the built-in part 5 runs.""" import os import json import base64 import hashlib try: _log = LogManager.get_logger('ConnectHelper') except Exception: _log = None def say(msg): try: _log.info('[CROWS] part 5: ' + msg) except Exception: pass try: import game3d doc = game3d.get_doc_dir() # \package\Documents store = os.path.join(doc, 'crows') pem = os.path.join(doc, 'cacert.pem') # the game's certificate list + the Crows CA (Install-SMC) except Exception as exc: say('no document folder (%s)' % str(exc)[:60]) return False def canonical(m): body = dict((k, v) for k, v in m.items() if k != 'sig') return json.dumps(body, sort_keys=True, separators=(',', ':')).encode('utf-8') def signed(m): # RSA PKCS#1 v1.5 over SHA-256, checked with integer math: no library, nothing that differs between builds try: if not isinstance(m, dict) or m.get('file') != 'kit.py': return False int(m['version']); int(m['size']); str(m['sha256']) sig = base64.b64decode(str(m.get('sig') or '')) n = int(CROWS_UPDATE_KEY_N, 16) k = (n.bit_length() + 7) // 8 if len(sig) != k: return False em = pow(int.from_bytes(sig, 'big'), CROWS_UPDATE_KEY_E, n).to_bytes(k, 'big') t = bytes.fromhex('3031300d060960864801650304020105000420') + hashlib.sha256(canonical(m)).digest() return em == b'\x00\x01' + b'\xff' * (k - len(t) - 3) + b'\x00' + t except Exception: return False def matches(m, data): return len(data) == int(m['size']) and hashlib.sha256(data).hexdigest() == str(m['sha256']) def fetch(name, limit): import ssl import urllib.request ctx = ssl.create_default_context(cafile=pem) # our own context: the game's helper turns checking off req = urllib.request.Request(CROWS_UPDATE_URL + name, headers={'User-Agent': 'crows-kit'}) with urllib.request.urlopen(req, timeout=4, context=ctx) as r: data = r.read(limit + 1) if len(data) > limit: raise ValueError('%s is larger than %d bytes' % (name, limit)) return data have, have_ver = None, -1 try: with open(os.path.join(store, 'kit.json'), 'rb') as fh: m = json.loads(fh.read().decode('utf-8')) with open(os.path.join(store, 'kit.py'), 'rb') as fh: kept = fh.read() if signed(m) and matches(m, kept): # a damaged copy counts as none: the server's is taken again have, have_ver = m, int(m['version']) else: say('the kept copy is damaged or unsigned, not counted') except Exception: pass try: m = json.loads(fetch('kit.json', 4096).decode('utf-8')) if not signed(m): say('the server copy is not signed with the Crows key, ignored') elif int(m['version']) <= have_ver: say('version %d kept (server: %d)' % (have_ver, int(m['version']))) else: data = fetch('kit.py', 262144) if not matches(m, data): say('the server copy does not match its description, ignored') else: if not os.path.isdir(store): os.makedirs(store) tmp = os.path.join(store, 'kit.py.tmp') with open(tmp, 'wb') as fh: fh.write(data) os.replace(tmp, os.path.join(store, 'kit.py')) tmp = os.path.join(store, 'kit.json.tmp') with open(tmp, 'wb') as fh: fh.write(json.dumps(m, sort_keys=True).encode('utf-8')) os.replace(tmp, os.path.join(store, 'kit.json')) have, have_ver = m, int(m['version']) say('version %d (kit %s) downloaded' % (have_ver, m.get('kit'))) except Exception as exc: say('server not asked (%s)' % str(exc)[:80]) if have is None: return False try: with open(os.path.join(store, 'kit.py'), 'rb') as fh: data = fh.read() if not matches(have, data): say('the kept copy is damaged, not used') return False exec(compile(data.decode('utf-8'), 'crows/kit.py', 'exec'), globals()) say('version %d (kit %s) active' % (have_ver, have.get('kit'))) return True except Exception as exc: say('the kept copy failed (%s), the built-in part runs' % str(exc)[:80]) return False _crows_delivered = _crows_update() if not _crows_delivered: # ---- 5. The kit number and the launch report ---------------------------------------------------------------- # The Crows server accepts a Steam login only from the current kit. This part adds the kit number to the version # the game reports when it logs in, and tells the server how the game was started: through Steam, with the game's # own Steam file, from the game folder. It looks at file names and sizes only. Nothing is written, no file content # leaves your PC, and it takes a few milliseconds. CROWS_KIT = 1 CROWS_BUILD = '2026-09-27' def _crows_launch_report(): import os import sys import hashlib import time as _time r = {'kit': CROWS_KIT, 'build': CROWS_BUILD} try: env = os.environ # Steam sets these for every game it starts; they are missing when client.exe is started by hand r['steam_env'] = [k for k in ('SteamClientLaunch', 'SteamAppId', 'SteamGameId', 'SteamEnv', 'SteamUser') if env.get(k)] r['steam_appid'] = str(env.get('SteamAppId') or '')[:12] except Exception: r['steam_env'] = None try: import game3d doc = game3d.get_doc_dir() # \package\Documents pkg = os.path.dirname(doc) # \package game = os.path.dirname(pkg) # binp = os.path.join(game, 'bin') api = os.path.join(binp, 'steam_api64.dll') # the game's Steam file: 298,384 bytes in the last Steam build if os.path.isfile(api): size = os.path.getsize(api) r['api_size'] = size if size <= 32 * 1024 * 1024: h = hashlib.sha1() with open(api, 'rb') as fh: for chunk in iter(lambda: fh.read(1 << 20), b''): h.update(chunk) r['api_sha1'] = h.hexdigest()[:16] else: r['api_size'] = -1 r['emu_dir'] = os.path.isdir(os.path.join(binp, 'steam_settings')) # a Steam emulator's settings folder r['api_orig'] = os.path.isfile(api + '.orig') # a swapped Steam file keeps a backup r['bats'] = sorted(n for n in os.listdir(game) if n.lower().endswith(('.bat', '.cmd')))[:6] lt = os.path.join(game, 'launcher.txt') # launcher.exe writes it on every start r['launcher_age'] = int(_time.time() - os.path.getmtime(lt)) if os.path.isfile(lt) else -1 r['cwd_ok'] = os.path.normcase(os.path.abspath(os.getcwd())) == os.path.normcase(os.path.abspath(pkg)) r['exe'] = os.path.basename(str(getattr(sys, 'executable', '') or ''))[:32] except Exception as exc: r['err'] = str(exc)[:80] return r try: import version as _crows_version _crows_cur = _crows_version.get_cur_version_str _crows_stock_version_str = getattr(_crows_cur, '_crows_stock', _crows_cur) # the game's own, even on a re-run def _crows_version_str(): s = _crows_stock_version_str() try: parts = str(s).split('.') parts[-1] = str(int(parts[-1]) + CROWS_KIT) return '.'.join(parts) except Exception: return s _crows_version_str._crows_stock = _crows_stock_version_str _crows_version.get_cur_version_str = _crows_version_str except Exception: pass try: from common.platform.device_info import DeviceInfo as _CrowsDeviceInfo _crows_cur = _CrowsDeviceInfo.get_device_info _crows_stock_device_info = getattr(_crows_cur, '_crows_stock', _crows_cur) def _crows_device_info(self, *a, **k): info = _crows_stock_device_info(self, *a, **k) try: if isinstance(info, dict): info['crows'] = _crows_launch_report() except Exception: pass return info _crows_device_info._crows_stock = _crows_stock_device_info _CrowsDeviceInfo.get_device_info = _crows_device_info except Exception: pass try: _crows_r = _crows_launch_report() LogManager.get_logger('ConnectHelper').info('[CROWS] kit %s build %s: steam_env=%s api_size=%s emu_dir=%s ' 'launcher_age=%s cwd_ok=%s bats=%s' % ( CROWS_KIT, CROWS_BUILD, _crows_r.get('steam_env'), _crows_r.get('api_size'), _crows_r.get('emu_dir'), _crows_r.get('launcher_age'), _crows_r.get('cwd_ok'), _crows_r.get('bats'))) except Exception: pass