{"slug": "an-llm-revived-a-2016-game-with-one-byte", "title": "An LLM revived a 2016 game with one byte", "summary": "An agent running Deepseek Flash v4.1 in the PI harness made a 2016 Android game, \"Army of Darkness Defense,\" installable on current Android versions by editing a single byte in the APK's binary manifest to raise its targetSdkVersion from 23 to 24, the minimum that clears the install gate on Android 15 (API 35), 16 (API 36) and 17 (API 37). The author, who states he has no Android development knowledge, set up the experiment after Android 14 rejected the APK with INSTALL_FAILED_DEPRECATED_SDK_VERSION; the agent first confirmed the app's signature checks were confined to bundled ad and in-app-purchase SDKs, making re-signing safe, then avoided a full apktool rebuild in favor of a width-preserving edit. The author argues the approach generalizes to enterprise legacy migrations, where the goal is the minimum viable compatibility rather than the newest SDK, since each targetSdk value above 24 activates behavioral rules the app was never written against.", "body_md": "A week ago I tried to install one of my favourite childhood games, “Army of Darkness Defense” from 2016, on my Android phone for the nostalgia. It was no longer on the Play Store, so I found the APK on the web. It refused to install:\n\n```\nINSTALL_FAILED_DEPRECATED_SDK_VERSION:\nApp package must target at least SDK version 24, but found 23\n```\n\nSince Android 14 the OS refuses apps built for ancient API levels, because malware historically targeted low `targetSdkVersion` to opt out of the runtime permission model. The app still works. The platform just stopped accepting it.\n\nThat is the same situation a lot of enterprise software is in. The software works, the vendor is gone or the upgrade costs a fortune, and the only thing standing between it and a supported machine is a compatibility gate that nobody dares open by hand. Usually the answer is a rewrite, a paid migration project, or a VM kept alive for another decade.\n\nI’d been watching people port PS5 games to PC with LLMs, and that was the thing that made me stop and think: the model wasn’t doing anything clever, it was just reading a lot of tedious format documentation and carefully getting details right. That is exactly the profile of a legacy migration. a non-creative Boring task which an Engineer must do here and there.\n\nSo I set up an experiment. I have no Android development knowledge at all. I gave the APK to an agent (Deepseek Flash v4.1 in the PI harness) and asked it to make the game install.\n\n**A note on whose work this is.** I didn’t solve an Android problem, I couldn’t have. The reasoning below is the agent’s, the decisions were its, and the commands are the ones it ran. My contribution was setting up the experiment, reading what it did, and occasionally stopping it from going down the wrong path.\n\n## The wrong instinct: newest is best\n    \n[#](#the-wrong-instinct-newest-is-best)\n\nfirst thing which comes to mind is to bump the SDK to 35 (latest). That is also how you turn a working game into a crash loop, it doesn’t work\n\n`targetSdk` is not a “how new am I” label, it’s a contract. Every value above 24 switches on another behavioural rule the app was never written against: `FileProvider` requirements at 24, background service restrictions at 26, cleartext HTTP blocked at 28, package visibility at 30. A 2016 game engine that loads assets from local paths and phones home to dead ad networks breaks in several of those.\n\nThe right question is not “what is newest” but “what is the *lowest* SDK that clears the gate”. That is a table, not a guess — and it plateaus at 24:\n\n| Android | API | minimum targetSdk to install | \n|---|---|---|\n| 14 | 34 | 23 | \n| 15 | 35 | 24 | \n| 16 | 36 | 24 | \n| 17 | 37 | 24 | \n\nSo 23 → 24 is the smallest change that clears every current and announced release. This is the part that generalises to enterprise migrations: the goal is the minimum viable compatibility, not the newest everything.\n\n## Check for anti-tamper first, not after\n    \n[#](#check-for-anti-tamper-first-not-after)\n\nIf the app verifies its own signature, re-signing it bricks it, and you only find out once it’s already broken. The agent checked before touching anything:\n\n```\nstrings -a classes.dex | grep -E \"GET_SIGNATURES|getPackageInfo|checkSignatures\"\n```\n\nThat hits, which looks alarming. So it asked *which class* calls them — grepping the 2049-class inventory for `PackageManager`, integrity or tamper returned nothing. The signature APIs were only reachable through bundled ad and IAP SDKs. Checking native code told the same story: the `getSignature` string sits next to in-app-purchase receipt parsing, not APK integrity. Verdict: safe to re-sign.\n\n## One byte, not a rebuild\n    \n[#](#one-byte-not-a-rebuild)\n\nThe obvious route is `apktool d` / `apktool b`, which re-encodes the whole binary manifest — string pool rebuilt, attributes reordered, resources re-serialised. For a one-number change that is a huge diff and a fresh chance to break something.\n\nInstead: a width-preserving in-place edit. `targetSdkVersion` is a 4-byte little-endian integer, and 23 and 24 occupy exactly the same space, so nothing else in the file has to move.\n\nThe manifest inside an APK is chunked binary XML, not text. Two details cost the agent its first two attempts — attribute offsets are relative to `attributeStart`, not `headerSize`, and the typed value sits at `+16` inside the attribute:\n\n```\na = node_off + 16 + attr_start + i * attr_size   # correct, not node_off + headerSize\nname  = strings[name_index]                      # → \"targetSdkVersion\"\nvtype = buf[a + 15]                              # 0x10 == TYPE_INT_DEC\nvdata = u32(buf, a + 16)                         # → 23\n```\n\nThen the entire change:\n\n```\nstruct.pack_into(\"<I\", buf, attr_offset + 16, 24)\n```\n\n## Proving it is the only change\n    \n[#](#proving-it-is-the-only-change)\n\nThis is my favourite part, because it is what makes the claim checkable rather than plausible:\n\n```\na = zipfile.ZipFile(src).read('AndroidManifest.xml')\nb = zipfile.ZipFile(dst).read('AndroidManifest.xml')\nprint(len(a) == len(b))                                    # True, 13196\nprint([(i, hex(a[i]), hex(b[i])) for i in range(len(a)) if a[i] != b[i]])\n# [(7180, '0x17', '0x18')]      ← one byte. 23 → 24.\n```\n\nOne byte out of 13,196. The equal-length check is what proves nothing else was silently re-encoded.\n\n## Static checks are not proof\n    \n[#](#static-checks-are-not-proof)\n\n`aapt2`, `apksigner verify` and `zipalign -c` all pass, and they still don’t prove the OS will accept the file. So it booted a real Android 15 image and did the A/B:\n\n``` bash\n$ adb install -r original.apk\nFailure [INSTALL_FAILED_DEPRECATED_SDK_VERSION: ... but found 23]\n\n$ adb install -r mod-target24.apk\nSuccess\n```\n\nThen confirmed it wasn’t just installed but running: `primaryCpuAbi=armeabi-v7a` on an arm64 image, both native libraries loaded, zero `FATAL EXCEPTION` or ANR lines. It boots to the title screen and plays.\n\n## The part the patch can’t fix\n    \n[#](#the-part-the-patch-cant-fix)\n\nThe game runs, but only on phones that still ship the 32-bit ARM ABI. The APK is `armeabi-v7a` only: there is no arm64 build of `libgame.so` and there never will be, because that library came out of a 2016 studio that no longer exists. That is the limit which the LLM can’t solve.\n\nOn the emulator it worked because the system image still carries the 32-bit linker:\n\n- Most phones still run 32-bit app code today, because Android carried the ABI forward for compatibility.\n- Newer flagships with 64-bit-only kernels dropped it entirely. There is no flag, no setting and no patch that brings it back — the CPU simply won’t execute the instructions.\n- Google has been tightening this from the other side too, pushing 64-bit requirements onto the Play Store.\n\nSo the honest summary of the result is: **the one-byte fix restores the app on hardware that still supports its ABI, and does nothing for hardware that doesn’t.** I made a 2016 app installable again on an Android 15 emulator, not on every Android 15 phone.\n\nFixing that properly means rebuilding the engine from source, or shipping a 32-bit translation layer, and neither is a manifest edit. This is the same wall as the last one that stops a lot of legacy migrations: the cheap, verifiable, one-byte problem and the expensive architectural problem look like they’re in the same category, and they aren’t. An agent can find the first in an evening. The second is a rewrite, or a decade of virtualisation, which is exactly the outcome I was arguing against at the top.\n\n## My Take\n    \n[#](#my-take)\n\nThe patch itself is one byte, and an experienced Android developer would have found it faster than the agent did. The interesting part is everything around it: reading the platform’s install gate as a contract instead of a version number, checking for anti-tamper before modifying instead of after, choosing a width-preserving edit over a rebuild, and verifying against a real device rather than trusting static tools. None of that is novel expertise. All of it is tedious, and all of it is checkable.\n\nThat is why I think legacy modernisation is a genuinely good fit for these tools, better than a lot of the tasks they get pointed at:\n\n- **The rules are documented and stable.** The Android install gate is a published table. So is most of what blocks old enterprise software: TLS versions, deprecated syscalls, removed library APIs, 32-bit-only binaries, unsupported runtime versions.\n- **The work is boring in an expensive way.** Nobody’s career is made by bumping a manifest field. It’s the kind of task that sits in a backlog for years.\n- **The minimum change is the right change.** Legacy software has usually survived for a reason. The agent’s instinct to find the smallest compatible step, rather than the newest everything, is the correct instinct for an enterprise too.\n\nThe caveats are real. This was a game with no anti-tamper and no data to lose, and the agent did get things wrong before I read what it was doing. Anything with a compliance surface, a licence server or customer data needs a person in the loop.\n\nThe whole thing took an evening, and most of that was the emulator being difficult rather than the patch. As someone with no Android knowledge I found it genuinely surprising how easy it was to do such a patch.", "url": "https://wpnews.pro/news/an-llm-revived-a-2016-game-with-one-byte", "canonical_source": "https://balochy.de/blog/an-llm-revived-a-2016-game-with-one-byte/", "published_at": "2026-11-09 15:12:34+00:00", "updated_at": "2026-11-09 15:22:48.055363+00:00", "lang": "en", "topics": ["ai-agents", "artificial-intelligence", "large-language-models"], "entities": ["Deepseek Flash v4.1", "PI", "Army of Darkness Defense", "Android 14", "Android 15", "Android 16", "Android 17", "apktool"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/an-llm-revived-a-2016-game-with-one-byte", "markdown": "https://wpnews.pro/news/an-llm-revived-a-2016-game-with-one-byte.md", "text": "https://wpnews.pro/news/an-llm-revived-a-2016-game-with-one-byte.txt", "jsonld": "https://wpnews.pro/news/an-llm-revived-a-2016-game-with-one-byte.jsonld"}}