Last active
June 22, 2026 07:28
-
-
Save kvnxiao/32ee4fcbd2364befeddee7c2a4bff5a0 to your computer and use it in GitHub Desktop.
Hatsukoi 1/1 NoCD patch applier / reverter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Hatsukoi 1/1 NoCD patch - applier / reverter. | |
| Forces the engine's disc-presence check (FUN_005f4500) to always report | |
| "disc present" by overwriting its entry prologue with `mov al,1 ; ret`. | |
| Keep THIS file (a few KB) instead of the 8.5 MB patched exe. After a reinstall, | |
| run it against the fresh original SiglusEngine.exe to re-apply. | |
| Usage: | |
| python apply_nocd_patch.py [apply|revert|check] [path-to-SiglusEngine.exe] | |
| - no path -> looks for SiglusEngine.exe next to this script, then in the | |
| parent folder (i.e. the game dir, if this file lives in _nocd_backup). | |
| - if action is omitted, it defaults to "apply". | |
| """ | |
| import os, sys, hashlib | |
| OFFSET = 0x1F3900 # .text file offset; VMA 0x5F4500 | |
| ORIG = bytes([0x55, 0x8B, 0xEC]) # push ebp ; mov ebp,esp | |
| PATCHED = bytes([0xB0, 0x01, 0xC3]) # mov al,1 ; ret | |
| # 13-byte build signature that follows the 3 patched bytes (unchanged by the | |
| # patch). Used to confirm we're looking at the known build before writing. | |
| ANCHOR_TAIL = bytes([0x6A, 0xFF, 0x68, 0x2C, 0x6C, 0x97, 0x00, | |
| 0x64, 0xA1, 0x00, 0x00, 0x00, 0x00]) | |
| # This is the English patched executable from https://tsurezurescans.wordpress.com/2018/09/27/hatsukoi-1-1-full-english-patch-main-game-append-scenarios/ | |
| ORIG_SHA256 = "2e48aa40b5f0db720c1f520effd4b39e364ecf16a66ed0611a991d49a513e8d3" | |
| def find_exe(): | |
| here = os.path.dirname(os.path.abspath(__file__)) | |
| parent = os.path.dirname(here) | |
| cands = [] | |
| # If this script lives in _nocd_backup\, the real game exe is one level up; | |
| # prefer it over the pristine backup copy that sits next to this script. | |
| if os.path.basename(here).lower() == "_nocd_backup": | |
| cands.append(os.path.join(parent, "SiglusEngine.exe")) | |
| cands.append(os.path.join(here, "SiglusEngine.exe")) | |
| cands.append(os.path.join(parent, "SiglusEngine.exe")) | |
| for cand in cands: | |
| if os.path.isfile(cand): | |
| return cand | |
| return None | |
| def state(data): | |
| window = data[OFFSET:OFFSET + 16] | |
| if len(window) < 16 or window[3:16] != ANCHOR_TAIL: | |
| return "unknown" # not the known build (updated/different exe) | |
| head = window[0:3] | |
| if head == PATCHED: | |
| return "patched" | |
| if head == ORIG: | |
| return "original" | |
| return "unexpected" | |
| def main(): | |
| args = [a for a in sys.argv[1:]] | |
| action = "apply" | |
| path = None | |
| for a in args: | |
| if a in ("apply", "revert", "check"): | |
| action = a | |
| else: | |
| path = a | |
| if path is None: | |
| path = find_exe() | |
| if not path or not os.path.isfile(path): | |
| sys.exit("ERROR: SiglusEngine.exe not found. Pass its path as an argument.") | |
| data = bytearray(open(path, "rb").read()) | |
| st = state(data) | |
| sha = hashlib.sha256(data).hexdigest() | |
| print("target : %s" % path) | |
| print("state : %s (sha256 %s%s)" % ( | |
| st, sha[:16], " == known-original" if sha == ORIG_SHA256 else "")) | |
| if st == "unknown": | |
| sys.exit("ABORT: bytes at 0x%X don't match the known build. This is likely a\n" | |
| " different/updated SiglusEngine.exe. Not touching it to avoid\n" | |
| " corruption. (Expected tail %s)" % (OFFSET, ANCHOR_TAIL.hex(" "))) | |
| if st == "unexpected": | |
| sys.exit("ABORT: unexpected bytes at the patch site; refusing to write.") | |
| if action == "check": | |
| print("OK (no changes made).") | |
| return | |
| if action == "apply": | |
| if st == "patched": | |
| print("Already patched. Nothing to do.") | |
| return | |
| data[OFFSET:OFFSET + 3] = PATCHED | |
| open(path, "wb").write(data) | |
| print("PATCHED: wrote %s at 0x%X (NoCD active)." % (PATCHED.hex(" "), OFFSET)) | |
| return | |
| if action == "revert": | |
| if st == "original": | |
| print("Already original. Nothing to do.") | |
| return | |
| data[OFFSET:OFFSET + 3] = ORIG | |
| open(path, "wb").write(data) | |
| print("REVERTED: wrote %s at 0x%X (original disc check restored)." % (ORIG.hex(" "), OFFSET)) | |
| return | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment