discover · compare · collections · requirements · contribute · feed · watching · stats · help with your tokens
Dreamucxe/GameCore · source
Snapshot 0 · 2026-11-04T12:47:28Z · Kotlin · MIT
Content fingerprint
36819ab68829a04711262aca56af3222ec3fab153fbd9eb2e1fe702c2d2fa47bGitHub description at capture
Android gaming overlay, performance monitor and per-game profile manager. Every number it shows is one the platform actually reported — where Android will not answer, it says so instead of guessing.
README at capture
<p align="center">
<img src="docs/assets/readme-hero.png" alt="GameCore — every number is a reading" width="100%">
</p>
<p align="center">
<a href="https://github.com/Dreamucxe/GameCore/releases/latest/download/GameCore.apk"><img alt="Download the signed GameCore APK" src="https://img.shields.io/badge/Download-Signed%20APK-7C5CFF?style=for-the-badge&logo=android&logoColor=white"></a>
<img alt="Android 8.0 or newer" src="https://img.shields.io/badge/Android-8.0%2B-3DDC84?style=flat-square&logo=android&logoColor=white">
<img alt="Kotlin 2.0" src="https://img.shields.io/badge/Kotlin-2.0-7F52FF?style=flat-square&logo=kotlin&logoColor=white">
<img alt="Jetpack Compose and Material 3" src="https://img.shields.io/badge/Compose-Material%203-4285F4?style=flat-square">
<img alt="MIT license" src="https://img.shields.io/badge/License-MIT-FF8FB1?style=flat-square">
<img alt="No root required" src="https://img.shields.io/badge/No%20root-required-2EA043?style=flat-square">
<img alt="No telemetry" src="https://img.shields.io/badge/No%20telemetry-offline-FF8FB1?style=flat-square">
</p>
<p align="center">
<sub><strong>No root</strong> · <strong>No account</strong> · <strong>No backend</strong> · <strong>No analytics</strong> · <strong>Sideload only</strong></sub>
</p>
<p align="center">
<a href="#at-a-glance">At a glance</a> ·
<a href="#screens">Screens</a> ·
<a href="#why-gamecore">Why GameCore</a> ·
<a href="#install">Install</a> ·
<a href="#whats-new-in-44">Latest release</a> ·
<a href="#plugins-are-data-not-code">Plugins</a> ·
<a href="#features">Features</a> ·
<a href="#security">Security</a> ·
<a href="#architecture">Architecture</a> ·
<a href="#release-history">History</a>
</p>
> [!IMPORTANT]
> **The rule:** if GameCore cannot measure it, it says so. A missing reading is shown as *unavailable*, with the reason — never as a zero, an estimate, or a green checkmark.
## At a glance
| | |
| :--- | :--- |
| **Platform** | Android 8.0+ (API 26) · `arm64-v8a` / `armeabi-v7a` |
| **Built with** | Kotlin 2.0.21 · Jetpack Compose · Material 3 · Hilt · Room + SQLCipher |
| **Codebase** | 1,068 Kotlin source files · roughly 253,000 lines (app, SDK, Studio) |
| **Tests** | 3,100+ JVM tests · 290+ test classes · no emulator required |
| **Plugins** | 31 official `.gcplugin` packages · 23 bundled in the app |
| **Network** | One optional, payload-free TCP latency probe · no backend |
| **License** | MIT |
## Screens
<p align="center"><sub>Live readings, per-game profiles, HUD layouts and theme controls — drawn from what the device actually reports.</sub></p>
<table>
<tr>
<td width="50%" align="center" valign="top">
<img src="screenshots/Screenshot_20260904-030020_GameCore.png" width="100%" alt="GameCore Home screen with CPU, RAM, battery, temperature, refresh rate, display and storage readings">
<br><sub><strong>Home</strong> — live device readings and setup status</sub>
</td>
<td width="50%" align="center" valign="top">
<img src="screenshots/Screenshot_20260904-030025_GameCore.png" width="100%" alt="GameCore Games screen with automatic profile detection">
<br><sub><strong>Games</strong> — per-game profiles that apply and unwind</sub>
</td>
</tr>
<tr>
<td width="50%" align="center" valign="top">
<img src="screenshots/Screenshot_20260904-030033_GameCore.png" width="100%" alt="GameCore HUD screen with overlay and layout controls">
<br><sub><strong>HUD</strong> — overlay modules and layouts you place yourself</sub>
</td>
<td width="50%" align="center" valign="top">
<img src="screenshots/Screenshot_20260904-030042_GameCore.png" width="100%" alt="GameCore Settings screen with theme, accent, overlay and monitoring controls">
<br><sub><strong>Settings</strong> — theme, accent, overlays and monitoring</sub>
</td>
</tr>
</table>
## Why GameCore
<table>
<tr>
<td width="50%" valign="top">
<h3>📊 Readings, not theatre</h3>
<p>Most “game boosters” show a number and hope. GameCore’s central type is <code>Observed<T></code>: a value names its source and precision, or an absence names its reason. There is no default, no fallback constant and no estimate dressed up as a measurement.</p>
</td>
<td width="50%" valign="top">
<h3>↩️ Profiles that undo</h3>
<p>Every profile field is nullable. <code>null</code> means <em>leave it alone</em>. GameCore records the reading before it changes anything, then puts exactly that back when the game exits — not a default it assumed.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>🔌 Plugins are data</h3>
<p>A <code>.gcplugin</code> is a package of JSON the host interprets. No scripts, no WebView, no native libraries, no network. A plugin can ask for a theme, a readout or a setting; it cannot run code inside GameCore.</p>
</td>
<td width="50%" valign="top">
<h3>🔒 Offline by default</h3>
<p>No account, no backend, no analytics, no crash reporting. The only outbound connection is an optional, payload-free TCP latency probe you can switch off.</p>
</td>
</tr>
</table>
## Install
<p>GameCore is a signed sideloaded APK. There is no Play Store listing and no account to create.</p>
<table>
<tr>
<td width="33%" valign="top">
<h3>1 · Download</h3>
<p>Get the signed APK from the <a href="https://github.com/Dreamucxe/GameCore/releases/latest">latest release</a>.</p>
</td>
<td width="33%" valign="top">
<h3>2 · Open it</h3>
<p>Allow installs from your file manager when Android asks, then tap the APK. Android 8.0 or newer.</p>
</td>
<td width="33%" valign="top">
<h3>3 · Optional access</h3>
<p>Shizuku, usage access and overlay access are optional. Each feature says what it needs before it asks.</p>
</td>
</tr>
</table>
From a computer with adb:
```bash
adb install -r GameCore.apk
```
> [!NOTE]
> Shizuku is optional. Without it, the app still runs; capabilities that need ADB-level authority say so and point at the setup screen instead of pretending to work.
## What's new in 4.5
<p><strong>A fourth panel shape, and a way to switch shapes mid-game.</strong> The left edge now has a home for the whole control panel, and the floating button can open a layout picker without leaving the game.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🎮 Turbo deck</h3>
<p>The <strong>Turbo deck</strong> layout pins one tall <strong>GAMETURBO</strong> plate to the left edge and keeps the game visible on the right. Quick capture tiles, Stats/Crosshair/HUD switches, the record button, the full action grid, levels, media and quick apps all live on the plate. Aspect ratio, refresh rate, colour, crosshair, levels, media and every action are unchanged.</p>
</td>
<td width="50%" valign="top">
<h3>🎛️ Layout picker in-game</h3>
<p>Long-press the floating button, or tap <strong>Layout</strong> in the quick sheet, to choose between <strong>Turbo wings</strong>, <strong>Centered</strong>, <strong>Split edges</strong> and <strong>Turbo deck</strong>. The current shape is marked, and the choice is shared with Settings and the panel's own Layout tile. Tap, drag and double-tap on the button are unchanged.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>↩️ Every earlier layout stays</h3>
<p>The centered panel keeps its stored width, and switching away and back restores it. Fresh installs still open on Turbo wings; an existing install keeps its layout until you choose another.</p>
</td>
</tr>
</table>
## What's new in 4.4
<p><strong>The control panel grows wings — and each wing gets a real instrument.</strong> Two angled plates fold in from the screen edges, the game stays visible between them, and every existing control is still there.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🪽 Two angled wings</h3>
<p>The new <strong>Turbo wings</strong> layout pins a bent plate to each screen edge with the game visible through the middle. Readouts, levels and media live on the left; the game-space action grid and quick apps live on the right. Aspect ratio, refresh rate, colour, crosshair, levels, media and every action tile are unchanged. Tap the centre or the close button to leave, or use the <strong>Layout</strong> tile to cycle Turbo wings, Centered and Split edges without opening Settings.</p>
</td>
<td width="50%" valign="top">
<h3>🎛️ CPU, GPU, RAM or temperature</h3>
<p>Each wing carries a bevelled 3D gauge. Pick what it measures in <strong>Settings › Overlay</strong>, or tap the caption under the gauge to cycle it in-game. Every choice draws a different object: an amber circuit ring for CPU, a cyan facet ring for GPU, a green memory-stack ring for RAM and a heat ring for temperature. The figure inside is always a real reading — an unavailable sensor shows <em>n/a</em> and the reason, never a zero.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>↩️ The older surfaces stay</h3>
<p>Centered and Split edges are still there, and the width you stored for the centered panel is preserved when you switch away and back. Fresh installs open on Turbo wings; an existing install keeps its current layout until you choose another.</p>
</td>
</tr>
</table>
## What's new in 4.3
<p><strong>Plugin Studio now builds the data-only capabilities GameCore actually runs</strong> — not decorative controls that vanish after export.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🧱 Six runtime-backed sections</h3>
<p>The shared Studio and <a href="docs/PLUGIN_MAKER.md">Plugin Maker 1.1.0</a> can combine schema-4 themes, read-only plugin screens, one bounded monitoring overlay, typed persistent settings, dashboard widgets and edge-triggered automations in one deterministic package. The manifest declarations, required permission requests and live-data scopes are derived from the content, and import round-trips the same six sections instead of dropping them.</p>
</td>
<td width="50%" valign="top">
<h3>🧪 Test without touching the device</h3>
<p>The Test workspace validates every section, checks static workflow conflicts, inspects the exact zip and SHA-256, reports local Android capabilities, and runs rules against clearly labelled simulated triggers and metrics. Simulation never calls Shizuku, a shell, an intent or the production action broker.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>🔒 An explicit capability boundary</h3>
<p>Screen and dashboard content stays read-only, overlay modules stay fixed-size and non-touchable, and rules stay inside the host's closed trigger/action vocabulary. Studio does not claim plugin-to-plugin messaging, touchable overlays, arbitrary code, generic setting bindings or release-key signing. The audited contract is documented in <a href="docs/PLUGIN_STUDIO_CAPABILITIES.md">Plugin Studio capabilities</a>.</p>
</td>
</tr>
</table>
## What's new in 4.2
<p><strong>This release turns plugin making into a visual studio</strong> — a standalone Plugin Maker app and the same editor inside GameCore, both writing the same deterministic package.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🎨 A plugin studio on the phone</h3>
<p><a href="docs/PLUGIN_MAKER.md">GameCore Plugin Maker 1.0.0</a> is a separate, sideloadable app for designing a theme without a computer. It has a colour wheel with HEX, RGB and HSL entry, recent colours, favourites and saved palettes; all thirteen theme roles; fourteen bundled faces and a type ramp; schema-4 glass; the schema-3 glitch block; eight starting templates; undo and redo; autosaved projects; and a device simulator for compact, phone, large and tablet widths. Saving goes through the system file picker, so it needs no storage permission.</p>
</td>
<td width="50%" valign="top">
<h3>🧩 Create a plugin inside GameCore</h3>
<p><strong>Plugins › Create a plugin</strong> opens the same editor in GameCore 4.2. One shared implementation means the standalone app and the in-app flow cannot drift apart: both write <code>manifest.json</code> and <code>theme.json</code>, both are deterministic byte for byte, and both hand the file to the system picker rather than installing it behind your back. The result installs from <strong>Plugins › Choose files</strong> like any other package.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>🔍 Check, import, inspect</h3>
<p>The Check tab measures WCAG contrast for every ink/surface pair, warns about translucent ink and extreme scales, scores the theme, and previews protanopia, deuteranopia and tritanopia. Export shows exactly what the zip will contain and its SHA-256 before you save. Import reads an existing theme package and lists every field it cannot write back — and refuses a package with other sections instead of silently dropping them.</p>
</td>
</tr>
</table>
## What's new in 4.1
<p><strong>This release is about everyone being able to read the app</strong> — fifty-one languages, chosen during setup — and about a first plugin being something you can build in two files.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🌍 Fifty-one languages</h3>
<p>Every screen, button and setup step can speak one of fifty-one languages, including right-to-left Arabic, Hebrew, Persian and Urdu. The picker is on the first setup step and later in <strong>Settings › Appearance</strong>; until you choose, GameCore follows the device language. A line with no translation yet falls back to English instead of vanishing, and the whole layout flips for a right-to-left language.</p>
</td>
<td width="50%" valign="top">
<h3>🧩 A plugin template that installs</h3>
<p><a href="plugin-template/README.md"><code>plugin-template/</code></a> is a complete theme in two JSON files, with no Android build and no Kotlin. <code>tools/build-plugin.py</code> checks the manifest against the same rules the app enforces and writes a deterministic <code>.gcplugin</code> plus its SHA-256. Edit the id, the colours and the name, and you have a package to sideload.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>🔧 Correctness fixes</h3>
<p>The energy estimate now averages the two middle sessions when a game has an even number of them, so it stops rounding every projection upward. Stutter reports keep the five <em>worst</em> frame times rather than the five latest. A five-pixel display-size reading is no longer reported as a different panel, and a resolution override below the usable size now names the panel it was measured against. The language catalogues have their own integrity test: every language must carry every key, with every positional marker intact.</p>
</td>
</tr>
</table>
## What's new in 4.0
<p><strong>This release is about the app's typeface becoming part of the theme</strong> — the Liquid Glass packages now change it everywhere — and about installing plugins in batches.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🔤 Whole-app font themes</h3>
<p>Nine Liquid Glass font packages now change the typeface everywhere, not only inside the plugin section. Each package is a complete theme that keeps the glass material and Control Center slider while applying one bundled face across the app; switching packages switches the whole-app font.</p>
</td>
<td width="50%" valign="top">
<h3>🔡 Eight more bundled faces</h3>
<p>Orbitron, Press Start 2P, VT323, Comic Neue, Cinzel, Inter, JetBrains Mono and Rubik join the Minecraft-inspired pixel face. All eight are open-source families bundled with their SIL Open Font License texts, so the font packages stay data-only and ship no font files of their own.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>📥 Pick several plugin files at once</h3>
<p>The plugin manager's file picker now takes more than one <code>.gcplugin</code> at a time. Every file you pick is read and shown to you first — which ones can be installed, and why the others cannot — and one confirmation installs them all. Installing still grants nothing: every permission starts off, and each one is yours to approve on that plugin's own screen.</p>
</td>
</tr>
</table>
## What's new in 3.6.0
<p><strong>This release is about the app changing with the day and with the game in front of you</strong> — without taking away the choices you made by hand.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🕒 Theme and dashboard schedule</h3>
<p>Settings gains daily time windows for the plugin theme and the Home dashboard. A window can cross midnight, and a per-game override wins while that game is tracked. The schedule is an override layer: when no window or game applies, the manual theme and <strong>Show GameCore stats on Home</strong> choice return exactly as they were.</p>
</td>
<td width="50%" valign="top">
<h3>🧩 Active plugins on Home</h3>
<p>A compact strip at the top of Home names the plugins that are actually running. It uses the same enabled, suspended, quarantine and compatibility rules as the Plugins screen, stays out of the way when nothing is running, and points at the global switch when that is what stopped everything.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>📦 Ten more official packs</h3>
<p>Night Shift, Colorblind Safe, Papercut, Minimal Vitals, Thermal Desk, Battery Badge, Cooldown Ladder, Exit Restore, Charger Boost and Stat Dictionary are in <code>dist/</code> and bundled with the app. All ten remain data-only: JSON and image assets read through the same codecs as any sideloaded package, with no scripts, network calls, shell, native code, WebView, arbitrary pixels or custom permissions.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>🔎 Plugin search and appearance</h3>
<p>Search installed plugins by name, id, developer, description or version. The plugin section also has its own font and text-size controls, including a bundled Minecraft-inspired pixel face, without changing the rest of the app.</p>
</td>
<td width="50%" valign="top">
<h3>🎨 The plugin section follows your theme</h3>
<p>The plugin manager, detail screen and automation history now use the theme the rest of GameCore is wearing. The safety switch, consent switches and removal dialog keep GameCore's own colours, so a plugin cannot restyle the controls that turn it off.</p>
</td>
</tr>
</table>
## What's new in 3.5.0
<p><strong>This release is about the two things a hard session leaves behind:</strong> a display you cannot read, and a stutter you cannot explain. Both now have an answer that says how sure it is.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>🖥️ Display changes ask first</h3>
<p>Every display-size change shows a fifteen-second countdown with <strong>Keep</strong> and <strong>Revert</strong>. Silence, or a process that dies before the answer, puts the previous size back. A size left unconfirmed across a restart is recovered at launch, and a notification explains what happened — the undo never depends on reaching the screen that set it.</p>
</td>
<td width="50%" valign="top">
<h3>🧭 Recovery console</h3>
<p>A display-recovery screen in Settings restores the panel’s own size, shows whether the elevated shell is reachable, and says plainly when nothing was recorded to put back. It is also where the launch notification leads.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>📈 Frame-time attribution</h3>
<p>Session reports name what was happening around each spike — thermal throttling, a capped refresh rate, battery saver, device load, high temperature, a profile that did not fully apply — and call a spike nothing explains <em>unexplained</em> instead of picking a cause.</p>
</td>
<td width="50%" valign="top">
<h3>🧾 Session timeline and receipt</h3>
<p>Every session keeps a timeline of profile applies, thermal and refresh changes, power-saver transitions, network spikes and frame-time spikes. The whole report can be rendered as a PNG receipt and shared through the same file provider as the session card.</p>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<h3>📦 New-device migration</h3>
<p>Backups carry an optional device snapshot, and a restore preview names the refresh rates, display sizes and Android version differences that may not mean the same thing on this device. Files written before this release still restore and read as unknown origin; the device section is optional and additive.</p>
</td>
<td width="50%" valign="top">
<h3>🔋 Energy budget per game</h3>
<p>The profile editor estimates a game’s battery cost from its finished sessions: the median percent per hour, how many sessions that rests on, and the playtime left at the current level when the device is not charging. When there is not enough measured history, it says how many more sessions it needs rather than inventing a figure.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>🌐 Network versus device attribution</h3>
<p>Session reports compare frame-time spikes against two measured lanes — TCP handshakes to the router and to a known host beyond it — and answer <em>beyond this device</em>, <em>device to router</em>, <em>network but not localised</em>, or <em>not supported by the network record</em>. No game server is contacted or claimed; the probe is labelled as a handshake, not a ping, everywhere it appears.</p>
</td>
</tr>
<tr>
<td colspan="2" valign="top">
<h3>🧩 Home stats can get out of the way</h3>
<p>Appearance gains <strong>Show GameCore stats on Home</strong>. Turning it off removes the built-in CPU, RAM, battery, temperature and refresh-rate tiles, so a plugin dashboard that already shows the same readings is not duplicated. Plugin dashboard widgets stay visible; the Device card, thermal warnings and Home actions remain.</p>
</td>
</tr>
</table>
## Plugins are data, not code
<p align="center"><strong>31 official packages today · 23 bundled · one <code>.gcplugin</code> each</strong></p>
<p>A plugin is a <code>.gcplugin</code> zip archive of JSON and image assets that GameCore reads. It can restyle the app, add a readout or expose a setting; it never runs inside GameCore and never gets a shell. The plugin directory can grow without widening what any one install can do.</p>
<table>
<tr>
<td width="50%" valign="top">
<h3>✅ A plugin can</h3>
<ul>
<li>Supply colours, corner rounding, type and a glass or glitch effect.</li>
<li>Add a bounded module over a game, or a screen built from host components.</li>
<li>Offer its own switches, choices and numbers, stored by GameCore.</li>
<li>Read only the data you grant it, one permission at a time.</li>
</ul>
</td>
<td width="50%" valign="top">
<h3>🚫 A plugin cannot</h3>
<ul>
<li>Run scripts, load a WebView, ship native code or open a socket.</li>
<li>Draw arbitrary pixels, touch another app, the shell or a protected setting.</li>
<li>Grant itself anything: a new plugin arrives switched off with nothing granted.</li>
<li>Style the Plugins screen you use to turn it off.</li>
</ul>
</td>
</tr>
</table>
| Themes & interface packs | Tools & readouts |
| :--- | :--- |
| Liquid Glass · True Glitch · Nebula · Nothing OS · Personal OS · Anime · Cyberpunk · Comic · Cybercity · Cybercore · Cybersigilism · Chromecore · Retro Futurism · Robot Core · RedMagic · Night Shift · Colorblind Safe · Papercut | Advisor · Automation History · Benchmark Suite · Dashboard Composer · Graphics Tuning · Sensitivity Lab · Minimal Vitals · Thermal Desk · Battery Badge · Cooldown Ladder · Exit Restore · Charger Boost · Stat Dictionary |
The full set lives in [`dist/`](dist/). Bundled copies are in [`app/src/main/assets/plugins/`](app/src/main/assets/plugins/). Start with the [format](docs/PLUGIN_FORMAT.md), the [SDK](docs/PLUGIN_SDK.md), the [starter template](plugin-template/README.md) (two JSON files and `tools/build-plugin.py`), the [Plugin Maker](docs/PLUGIN_MAKER.md) (a standalone Android app that writes a package on the phone), and the [threat model](docs/PLUGIN_THREAT_MODEL.md).
---
## Features
For a screen-by-screen walkthrough, setup paths, permission map and troubleshooting guide, read the
**[GameCore 4.5 feature map and user guide](docs/GAMECORE_FEATURE_GUIDE.md)**. Plugin authors can use the
separate **[Plugin Studio feature map and tutorial](docs/PLUGIN_STUDIO_FEATURE_GUIDE.md)**.
### Feature map
| Area | What it covers |
| :--- | :--- |
| **Overlay** | Floating button, quick sheet, full control panel, dock, stats pill, crosshair, HUD builder |
| **Profiles** | Per-game refresh rate, brightness, display size, orientation, volume, DND, overlays, HUD, colour and session tracking |
| **Monitoring** | Per-core CPU, memory, battery, thermal, GPU, display, touch sampling, network and storage |
| **Sessions** | Encrypted history, graphs, timeline, frame-time attribution, network verdicts, shareable cards and receipts |
| **Aim Lab** | First-person 3D arena, seven training modes, weapon editor, records and exports |
| **Plugins** | Data-only themes, widgets, screens, settings and overlay modules |
The rest of this section is the long version: every overlay, profile field, reading and session rule, with the edge cases named rather than hidden.
### Appearance
Follow system, Dark, Light or AMOLED black, with a fixed palette of eight accents or a custom
colour of your own. The choice applies to the whole app and to the overlay windows the service
draws over your game, so the two never disagree.
### Floating overlay
- A draggable gaming button that snaps to any of four corners — or to the nearest edge on a free
drag — remembers its place separately for portrait and landscape, survives rotation, and says
what it knows without being opened: dimmed after a few seconds idle, with a small orange or red
dot when the device is hot or critical.
- Tapping it opens a **quick sheet** on the screen edge nearest the button: up to six toggles you
pin yourself, brightness and media volume, the game's own icon and the session clock. It is
narrow on purpose, so the game stays visible beside it.
- Behind **More** — or an optional double tap on the button — is the full panel, in **Display**,
**Overlays**, **Capture** and **Session** tabs: brightness and media-volume sliders, saturation,
contrast and hue, screenshot, screen recording, Do Not Disturb, orientation lock, flashlight,
colour presets, display shape, refresh rate, a shortcut back into the game, and a
hold-to-confirm End session. The panel has four shapes — **Turbo wings**, **Centered**,
**Split edges** and **Turbo deck** — and the shape is chosen in Settings or in-game: long-press
the floating button, or tap **Layout** in the quick sheet, to pick one without leaving the game.
- A configurable performance pill, as a compact single line or a detailed card: pick which stats
it shows and set its position, size, opacity, corner radius, text size and update interval. It
samples only while something is looking at it — and not at all once the screen is off.
- A crosshair overlay with eleven designs — cross, dot, ring, ring-and-dot, cross-in-ring, T,
X, chevron, corner brackets, box, or a PNG you import — and independent control of size,
thickness, centre gap, rotation, opacity, colour and screen position. The drawn designs
use Compose primitives, so they stay crisp at any size and ship no bitmaps.
- A visual HUD builder: drag widgets onto a live preview, choose from 18 stats, set each
widget's text size, opacity, colour, label and background, and save layouts that a game
profile can raise by name.
- A separate, lighter floating **dock**: a small draggable strip that snaps to a screen edge and
carries one-tap shortcuts to controls you already have. Which buttons it shows, and their order,
is yours to set and is saved per game, alongside the full panel rather than in place of it.
### Game profiles
One profile per package, holding target refresh rate, brightness, display size, orientation
lock, screen timeout, media volume, Do Not Disturb, which overlays to raise, a HUD layout, a
crosshair preset, a colour preset, a performance mode, whether to free memory on launch, and
whether to track the session — plus the three 3.5 smart features: thermal auto-downshift, the
network check, and holding full performance under battery saver. All three are off unless you
turn them on for that game.
Every adjustable field is nullable, and **null means leave it alone** — not "use a
default". A profile that sets only brightness records what brightness was, changes it, and
puts exactly that back when the game exits. Nothing else is touched, so nothing else can be
restored to a value the device never had.
Automatic detection applies a profile when its game comes to the foreground and unwinds it
when the game leaves. You can also launch a game straight from its profile screen.
**Free RAM on launch** is the one setting whose effect lands on your other apps, so it is off
by default, per game, and it runs last — after the overlay is up and the profile is applied,
because those are the things you are waiting for. GameCore never closes itself, the game, your
launcher, your keyboard, anything holding a foreground service (music, a recording, a
download, a navigation route), any part of the system, or anything on your never-close list in
Settings. With Shizuku it enumerates what is actually running, closes through the elevated
shell and re-reads the list to confirm; without it, it asks Android through
`killBackgroundProcesses()`, which reports nothing about what it did — so the summary says
*"asked Android to close"* rather than claiming a count it cannot verify. The freed figure is
two `ActivityManager` readings with the pass between them, and it is allowed to say it could
not measure, or that available memory did not rise.
### Monitoring
Per-core CPU usage and frequency from `/proc/stat` and `sysfs`, memory from `/proc/meminfo`
and `ActivityManager`, battery level, charging source, health, temperature, voltage and
instantaneous current, thermal status, GPU load where the device exposes a `/sys` node for it,
display mode and rotation, the touch sampling rate where the device will report it, network type, latency and
throughput, and free storage — with live graphs, a configurable sampling interval, and
sampling that stops the moment nothing is looking at it.
Battery drain is reported as **percent per hour** rather than raw points lost, and only
after five minutes of discharging, so a two-minute session cannot extrapolate to a
headline figure.
### Sessions
Every tracked session is written to an encrypted Room database: the game, start and end
time, duration, battery delta, average and peak CPU load, average and peak memory use,
average and peak temperature, the refresh rate it ran at, the network it used, what its
latency probes did, and the colour preset and values the display was held at. A session
recorded before 1.1 reads back as no colour reading rather than as a neutral one — an absence,
not an invented zero.
The session report draws those as graphs. The history screen filters, sorts, deletes and
aggregates them. A session interrupted by the app's process being killed is repaired on the
next launch and marked as ended by process death, rather than being silently dropped or
left running forever.
**What the connection did, across the whole session.** The latency probe answers "how is it
right now" from a burst of handshakes. A tracked session also keeps what happened across all
of them: how many completed, how many did not, how many came back at least twice the
session's own running average *and* at least 40 ms above it, the worst reading, the average
spread between consecutive probes, and the longest unbroken run of failures. The report turns
that into one word — *Stable*, *Spiky*, *Unreliable*, *No reply*, *Not measured* — and one
sentence naming the figures behind it. The two are not symmetric on purpose: "stable" is
withheld until at least three probes have gone out, while a single handshake that did not
complete is reported at once. Two quiet probes are not evidence of a good connection; one
refused handshake is evidence of a bad moment. A spike is measured against the session's own
average rather than a fixed threshold, because 180 ms is an event on a connection that has
been sitting at 30 ms and unremarkable on one that has been sitting at 190 ms. None of it is
called packet loss.
**A card you can share.** The report has a share action that draws a 1080×1260 PNG on the
device — game, date, duration, average frame rate, average heat, battery drain — and hands it
to the standard share sheet as a one-shot read grant through the app's own non-exported
`FileProvider`, never as a file path. The card keeps the rules of the screen it came from,
with less room to explain them: a reading the device could not take is an em dash and never a
zero, an interrupted session's duration is marked as a lower bound, a per-hour battery figure
is only printed when the session was long enough to support one (below that the tile becomes
the points actually lost, which is a subtraction of two real readings), and the sample count
travels with the averages — four samples and four hundred are different claims, and a card is
read alone in a chat window with nothing beside it to qualify it. The game's own label is the
one string on it GameCore did not write, so it is stripped of control characters, collapsed
and clamped before being drawn, and it contributes nothing to the filename.
### Game storage
What each of your games is holding in cache, and the one part of it GameCore is willing to
delete. Every cache cleaner on a store shows one number and one button; the number is usually
the whole data directory and the button usually runs `pm clear`, which is why the reviews of
those apps are full of people who lost a save. So each game gets three figures instead of one:
- **Cache** — what the platform reports for the package, the same number as that app's own
storage page in Settings.
- **Clearable** — the part of it GameCore can actually reach: the shared-storage cache
directory, and nothing else. A separate reading rather than a fraction of the first, because
Android 11 and below will not break the total down, and guessing the split would be guessing
how much a button is about to free.
- **Untouched** — the rest. Saves, logins, settings, and the assets a game downloaded after
install. Nothing GameCore does touches it, and it is on screen so that "freed 900 MB" can
never be read as having come out of it.
The list is your games — an app that declares itself one, or an app you made a profile for,
which is better evidence than the manifest — ordered largest cache first, with rows whose size
could not be read placed last rather than treated as empty.
A tap reads the cache figure, deletes `Android/data/<package>/cache`, waits for the platform's
accounting to catch up, and reads the figure again. What it reports is **the difference between
those two readings**: not the size that was there beforehand, and not the exit code of the
delete, since `rm -rf` on an empty directory and on two gigabytes exit identically. The figure
is allowed to come out at nothing — the reachable part may already have been empty, or the game
may have refilled it in the second it took to look, and both are ordinary.
The copy of the cache inside the game's private data directory is out of reach: a shell running
as the shell user cannot open another app's data directory, and GameCore holds no root path to
one. That part is reported as remaining, next to a button for Android's own storage page for
the app, which needs nothing granted and reaches more than this does. Measuring needs usage
access, deleting needs Shizuku, and when either is missing the screen says which rather than
offering an action that would achieve nothing.
### Refresh rate, honestly
Only rates the panel actually advertises are offered — the list comes from
`Display.getSupportedModes()`, not from a hardcoded 60/90/120.
A change is reported as applied **only after it has been read back and confirmed.** This
matters for a specific, verified platform behaviour: on several MediaTek and PowerVR
devices the standard `preferredDisplayModeId` / `Surface.setFrameRate()` calls are accepted
without error and then ignored. An app that reports success because the setter did not
throw is lying to its user on every one of those devices. GameCore instead reports
*"accepted the request but stayed at 60 Hz"* and offers the Shizuku path, which does work
there.
Frame rate gets the same treatment. Reliable per-frame FPS is generally not available
through public APIs without root or instrumentation, and what is available varies by OEM
and GPU. GameCore detects whether a usable signal exists on the device it is running on,
and shows *"Not available on this device"* when it does not — rather than estimating.
---
## What makes it different
### The non-negotiables
| ✅ GameCore does | 🚫 GameCore refuses |
| :--- | :--- |
| Measure before it changes, then restore the reading it found. | Fabricate a reading, use a fallback constant, or dress an estimate as a measurement. |
| Say “Unavailable”, “Requires Shizuku” or “Not supported by this device” with the reason. | Claim a system call worked because the setter did not throw. |
| Close only what a profile asked for, with a never-close list. | Root, inject code, read another process’s memory or interfere with anti-cheat. |
| Delete one game’s shared-storage cache after measuring it. | Run “clean everything”, `pm clear`, or touch a save, login or downloaded asset. |
| Draw share cards and receipts on the device. | Upload sessions, analytics or crash reports. |
| Make one optional, payload-free TCP handshake. | Invent a packet-loss percentage or contact a game server. |
Most "game booster" apps are theatre. They show a RAM figure, animate a progress bar, kill
some processes Android immediately restarts, and report success. GameCore does have a
switch that closes background apps when a game starts — off by default, per game, and it
tells you what it measured afterwards rather than what it hoped for, including when the
measurement failed or came out negative. The difference is not the capability. It is that
the number is a reading and not a flourish. Its central type is
```kotlin
sealed interface Observed<out T> {
data class Value<T>(val value: T, val source: DataSource, val precision: Precision)
data class Restricted(val reason: RestrictionReason, val unlockedBy: AccessLevel?, ...)
data class Failed(val detail: String, val cause: String?)
}
```
A reading is either a value that names where it came from and how exact it is, or an
absence that names its reason. There is no default, no fallback constant and no estimate
dressed up as a measurement. The consequence is visible in the app: some cards say
*"Not available on this device"* or *"Requires Shizuku"* instead of showing a number. That
is the feature, not a gap.
### Things this app deliberately does not do
- **No root, no injection.** No reading or writing another process's memory, and no anti-cheat
interference.
- **No fabricated statistics.** No claiming a system call worked when it did not.
- **No timer that closes apps.** Nothing is closed without a profile asking for it — never your
launcher, your keyboard, the game itself, anything holding a foreground service, or anything on
your never-close list.
- **No thermal limit override, and no protected system files.**
- **No “clean everything”.** The storage screen deletes one named directory belonging to one game
you tapped, and never a save, a login or a downloaded asset.
- **No packet-loss percentage.** A refused TCP handshake is not a dropped packet.
- **Nothing you record is uploaded.** The shareable session card is drawn on the device and handed
to your own share sheet.
The one thing that does write a game's own files is the config editor, and it stays inside that
game's shared `Android/data/<package>/files` directory — the config file you opened and edited,
its original backed up before the first change, each write shown to you line by line and committed
whole or not at all. Never its code, never a save in the private storage a shell running as `shell`
could not reach anyway, never a path that a `..` could climb out of, and never without Shizuku and
a one-time notice you accept.
The full set of shell commands the app can construct lives in one file
(`core/shizuku/ShellCommand.kt`) and a unit test enumerates every one of them and asserts
that `cmd`, `su`, `setprop`, `force-stop`, `pm clear`, `pm trim-caches`, `install` and
friends are unreachable at any privilege level. Most of what that file can build reads or
writes something about *this device* — a settings key, the display size, GameCore's own
permissions. A few reach further, and each is bounded to a form the test pins literally so
that a second use cannot be added without it failing: `am kill --user current <package>`
closes one game's background processes; `rm -rf /storage/emulated/<user>/Android/data/<package>/cache`
clears one game's shared cache; `taskset -ap <mask> <pid>` pins one game's process to a set of
cores; `input tap` and `input swipe` send one synthetic tap or press-and-hold to a per-game
screen point the user placed, for the volume-button trigger — no text, no key event, and no other
`input` subcommand is expressible; the charge-bypass toggle writes a single digit to one validated
`/sys/class/power_supply/<supply>/<node>` — the one command in the app that runs `sh -c`, and it
passes the value (`0` or `1`) and the path as positional arguments (`$1`, `$2`) rather than splicing
either into the script text, after a `test -w` probe and a `cat` read of that same node; the GPU HUD
reads one validated GPU-load node under `/sys` with `cat`; and the config editor's `ls`, `stat`,
`cp`, `mv` and `rm -f` read and rewrite files inside one game's own `Android/data/<package>/files` —
never its `cache`, never its `obb`, never the private storage a `shell`-uid process cannot reach, and
never through a path a `..` could climb out of. Every argument that originates outside the app's own
code is validated against a form before a command exists; every command but that one charge write
reaches `exec` as an argument vector; and that write runs a fixed script whose only inputs are its
two positional arguments, so there is nowhere a glob is expanded or a word is split.
---
## Colour correction
A display colour screen, with the same values reachable from inside a game.
- **Eleven values.** Red, green and blue gain; gamma as one slider or three; saturation,
contrast, hue rotation and a brightness offset. Every slider shows its number, and tapping
the number opens a keypad for an exact entry — held to the field's own range, so a hue turns
±180° while a gain stops at ±100%, and an out-of-range entry is refused rather than quietly
clamped to something you did not type.
- **Presets you own.** Seven ship — Vibrant, Warm, Cool, Night, Protanopia, Deuteranopia,
Tritanopia — and they are ordinary saved rows, not a fixed menu: open one, move a slider,
save it under a new name, rename it, delete it. There is no cap on how many you keep.
- **Per game.** A profile can carry a colour preset. It is applied when the game comes to the
front and put back when it leaves, from the reading taken before the change — the same
restore rule every other profile field follows.
- **In the overlay panel.** Saturation, contrast and hue sit directly under the existing volume
and brightness sliders, a Colour tile joins the action grid, and a long press on it drops your
saved presets in as chips. Everything applies as the slider moves, with no confirm step.
- **In the session report.** Which preset, and which values, were active while the session ran.
And the reason the feature is built the way it is: **Android exposes no per-channel colour
matrix to an app.** `ColorDisplayManager`'s matrix and `SurfaceControl.setDisplayColorTransform`
are hidden platform API that neither `WRITE_SECURE_SETTINGS` nor a shell running as uid 2000 can
reach. So GameCore projects what you asked for onto the sinks that do exist on the device it is
running on — night-display warmth, the display colour mode, the daltonizer,
reduce-bright-colours, colour inversion — and the screen lists every write it would make, names
each value this device has no sink for, and says why. A gamma slider that moved and quietly did
nothing would have been easier to build, and would have been exactly the lie this project exists
to avoid.
Colour correction needs `WRITE_SECURE_SETTINGS`, which Android never grants an app on its own —
Shizuku or a wireless-ADB pairing is the only way to hold it. Without it the screen says so and
points at the setup, rather than presenting sliders that write nothing.
---
## Shizuku is optional
> [!NOTE]
> **Shizuku is optional.** It unlocks the device-wide capabilities listed below. Without it, GameCore says which ones are unavailable and points at setup instead of faking a result.
Shizuku (or a wireless-ADB pairing) raises the app to ADB-level authority, which is what most
devices require for a *device-wide* refresh-rate change, for animation scales, for the display's
colour keys, for reading and reshaping the display's own size, for deleting a game's
shared-storage cache, for reading and editing a game's own configuration files, and for a handful
of `dumpsys` reads Android does not expose to ordinary apps.
What the app can run with that authority is a closed, enumerated set:
| Program | Used for |
| --- | --- |
| `id` | confirming the shell's own uid before trusting it |
| `cat` | `/proc/stat`, `/proc/meminfo`, a battery charge-control node, and a GPU-load `/sys` node where the device exposes one |
| `dumpsys` | `thermalservice`, `battery`, `display`, `SurfaceFlinger --latency`, `activity`, `gfxinfo` |
| `settings get/put` | nineteen specific keys, each with its own value range |
| `getprop` | three `ro.*` chipset properties |
| `pm grant` | three permissions, **to this app only** |
| `appops set` | two app-ops, **to this app only** |
| `wm size` | reading the display's size, setting a per-game override, and clearing it |
| `taskset -ap` | reading one game process's CPU affinity, and pinning it to chosen cores |
| `input tap` / `input swipe` | tapping, or press-holding, one saved per-game screen point — the volume-button point trigger's one action |
| `am kill` | closing one named background app, when a profile asks to free memory |
| `rm -rf` | one game's shared-storage cache directory, on a tap in Game storage |
| `ls` / `stat` | listing and sizing files inside one game's own config directory |
| `cp` / `mv` | staging an edited config to a temp file and committing it over the original atomically |
| `rm -f` | removing one staged temp, or one config file you deleted in the editor |
| `test -w` | probing whether a device's battery charge-control node exists and is writable, before offering the toggle |
| `sh -c` | writing a single digit (`0`/`1`) to that charge-control node — the one shell command in the app, with the value and path passed as positional arguments (`$1`, `$2`) rather than spliced into the script |
The one shell command in that list runs a fixed script and receives the value and the path as
positional arguments (`$1`, `$2`), never spliced into the script text, so there is no place for a
value or a path to break out of the `echo` or its redirection. Every other command reaches `exec`
as an argument vector, with no shell at all. Every
argument that originates outside the app's own code — a package name from a stored profile,
a value from a slider — is validated against a form before a command is built, and a
rejected argument produces no command at all rather than a malformed one. `pm grant` and
`appops set` are checked against the compiled application id, so neither can be aimed at
another app.
---
## Permissions, and why each one is there
> [!TIP]
> Nothing is requested until the feature that needs it is switched on. Every permission below is also explained in the app’s own Permissions screen, with a button to the exact system page that grants it.
| Permission | What stops working without it |
| --- | --- |
| `SYSTEM_ALERT_WINDOW` | the floating button, pill, crosshair and HUD |
| `PACKAGE_USAGE_STATS` | automatic game detection and apply-on-launch, and cache sizes |
| `QUERY_ALL_PACKAGES` | listing installed games so you can make a profile |
| `KILL_BACKGROUND_PROCESSES` | "free RAM on launch" on a device without Shizuku |
| `WRITE_SETTINGS` | brightness, screen timeout and orientation lock |
| `ACCESS_NOTIFICATION_POLICY` | the Do Not Disturb toggle |
| `MODIFY_AUDIO_SETTINGS` | the volume slider |
| `WRITE_SECURE_SETTINGS` | colour correction (granted through Shizuku or adb only) |
| `ACCESS_NETWORK_STATE`, `INTERNET` | network type, latency and throughput |
| `POST_NOTIFICATIONS` | any foreground service, since each must post one |
| `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` | detection and tracking surviving Doze |
| `FOREGROUND_SERVICE_*` | the app's foreground services |
| Shizuku `API_V23` | the elevated capabilities, if you use Shizuku at all |
Each foreground service declares its real Android 14 foreground-service type rather than one
generic value: `dataSync` for session tracking, `mediaProjection` for screen recording with
its own separate consent flow, and `specialUse` — with a manifest justification for each — for
the overlay, game-detection, performance-monitoring and shake-shortcut services, which is the
closest the platform offers. Nothing runs when it is not needed — each service stops itself.
---
## Security
### Security at a glance
| Layer | Guarantee |
| :--- | :--- |
| **APK** | Release builds go through R8 with obfuscation and resource shrinking. |
| **Database** | Room is encrypted with SQLCipher; the key lives in the Android Keystore. |
| **Preferences** | `EncryptedSharedPreferences`; `android:allowBackup="false"`. |
| **Intents** | Only the launcher activity is reachable by another ordinary app; no exported receivers, no deep links. |
| **Files** | A non-exported `FileProvider` grants one URI at a time; no storage permission. |
| **Network** | Cleartext is blocked; the only outbound connection is the optional latency probe. |
| **Root** | Detected, never required and never exploited. |
- **Release builds go through R8** with obfuscation and resource shrinking, so a decompiled
APK does not hand over the optimization engine's internals or the shell command structure.
- **The Room database is encrypted with SQLCipher**, and its key is generated in and held by
the Android Keystore — not stored in a file the app could leak. Preferences go through
`EncryptedSharedPreferences`.
- **`android:allowBackup="false"`.** The session database and encrypted preferences never
leave the device through a backup transport.
- **The only intent filter an app can invoke is the launcher activity's.** The three others in
the manifest are system-dispatcher actions on services the system binds, never another app: the
notification listener that reads the active media session, the Quick Settings tile, and the
optional accessibility service behind the in-game volume-key trigger. Each is guarded by a
`BIND_*` permission the system alone holds and no `<uses-permission>` requests, so being exported
makes it reachable by `system_server` and nothing else. No exported receivers and no deep links,
and every activity but the launcher is `exported="false"`. Two content providers exist and
neither is a way in: the AndroidX `FileProvider` that hands out screenshots, recordings, exports
and session cards is not exported and grants a read on one URI at a time, and the Shizuku startup
provider must be exported for Shizuku's own server process to bind it, so it is guarded by
`INTERACT_ACROSS_USERS_FULL` — a signature-level permission no ordinary app holds.
- **The files that leave the device leave by your hand.** Screenshots, recordings, session
exports and session cards are written inside the app's own storage and reach another app only
through the share sheet, as a content URI with a one-shot read grant rather than a path, which
is why GameCore asks for no storage permission on any API level. A card's name is a timestamp
and nothing else — a game's own label has no business in a path — at most four are kept before
the older ones are deleted, and the backup rules exclude the whole of `files/` from both cloud
backup and device transfer.
- **No cleartext traffic**, enforced by a network security config. The only outbound
connection the app ever makes is the TCP latency probe, to a host you can change or switch
off in Settings.
- **All external text is sanitized before storage and again before render.** A game's
`android:label` is another developer's string and an imported HUD layout is a file from
anywhere, so both pass a filter that strips bidi overrides, zero-width characters, control
characters and combining-mark stacks, and truncates on a grapheme boundary rather than
mid-surrogate. There is a unit test per hostile input, with each invisible character built
from its code point so the test file stays reviewable.
- **No security-relevant behaviour branches on `BuildConfig.DEBUG`** or on a preference, and
no shell output, session data or device identifier is logged in a release build.
- **Root is detected, not required and not exploited.** If the sandbox is not intact,
GameCore says so plainly rather than continuing to assume its own guarantees hold.
---
## Architecture
```mermaid
flowchart LR
UI["Compose UI<br/>Screens · State · ViewModel"] --> Domain["Domain<br/>profiles · sessions · display"]
Domain --> Repo["Repositories"]
Repo --> DB[("Encrypted Room<br/>SQLCipher")]
Domain --> Platform["Android APIs<br/>readers · controllers"]
Domain --> Shell["Shizuku shell<br/>optional · enumerated"]
Plugin[".gcplugin<br/>data only"] --> Host["Plugin host"]
Host --> UI
```
Kotlin only, Jetpack Compose with Material 3, MVVM, Hilt, Coroutines and `StateFlow`. No
Java, no XML layouts — the only XML is Android resources and the manifest. 701 Kotlin source
files in the app and 30 in the plugin SDK, about 184,000 lines together.
<details>
<summary>Package tree</summary>
```
app/
├── core/
│ ├── common/ Observed<T>, AccessLevel, Formatters, TextSanitizer
│ ├── model/ the domain types — profiles, HUD, colour, sessions, readings
│ ├── permissions/ the catalog and live permission state
│ ├── system/ readers and controllers over the platform APIs
│ ├── overlay/ window geometry, drag/snap arithmetic, the overlay hosts
│ └── shizuku/ the elevated shell, its command set, root detection
├── data/
│ ├── database/ Room + SQLCipher, entities, DAOs, mappers
│ ├── preferences/ EncryptedSharedPreferences, the Keystore-held DB key
│ └── repository/ the only thing the domain layer talks to
├── domain/
│ ├── color/ the colour values, and which sink each one can reach
│ ├── display/ refresh rate and display size, applied and read back
│ ├── gaming/ game detection, profile apply and unwind, session tracking
│ ├── memory/ which apps a reclaim pass may close, and what it measured
│ ├── monitoring/ sampling, aggregation, thermal, battery and latency trends
│ ├── optimization/ OptimizationManager over Standard / Shizuku optimizers
│ ├── overlay/ what is on screen and why
│ └── storage/ per-game cache measurement, and the one delete it offers
├── service/ six foreground services, the media listener, and the quick-trigger tile and accessibility services
└── ui/ one package per screen, each a Screen + State + ViewModel
```
</details>
The UI never executes a shell command or touches a system API directly; it reads state and
calls into the domain layer. `OptimizationManager` chooses between `StandardAndroidOptimizer`
and `ShizukuOptimizer` based on what `DeviceCapabilityChecker` found, and every operation
comes back as one of *available*, *requires Shizuku*, *requires a permission*,
*unsupported*, *applied* or *failed* — there is no boolean that means "probably".
---
## Building
| Task | Command |
| :--- | :--- |
| Debug APK | `./gradlew :app:assembleDebug` |
| JVM unit tests | `./gradlew :app:testDebugUnitTest` |
| Signed release | `./gradlew :app:assembleRelease` |
| Lint | `./gradlew :app:lintDebug` |
```bash
git clone https://github.com/Dreamucxe/GameCore.git
cd GameCore
./gradlew :app:assembleDebug # installable debug APK
./gradlew :app:testDebugUnitTest # JVM unit tests, no device needed
./gradlew :app:assembleRelease # R8 + shrinking; signed if you have a key
./gradlew :app:lintDebug # zero warnings is the standard here
```
Kotlin 2.0.21 · AGP 8.5.2 · Gradle 8.9 · JDK 17 · compileSdk 34 · minSdk 26.
`assembleRelease` produces a **signed** APK when a `keystore.properties` is present at the
project root:
```properties
storeFile=release.keystore
storePassword=…
keyAlias=…
keyPassword=…
```
Without it the build still succeeds and emits an unsigned APK, so a contributor with no
signing key is not blocked — but an unsigned APK will not install on a device.
The unit tests are deliberately written against the pure, Android-free seams: the geometry,
the formatters, the sanitizer, the command builder, the aggregators, the state reducers, the
per-session latency fold, and every word the shareable card is allowed to print. 271 test
classes, 3,181 tests. No mocking framework, no Robolectric, no emulator — the suite runs on any JDK.
---
## Your data, and the network
| Data | Where it lives | Leaves the device? |
| :--- | :--- | :--- |
| Profiles, HUD layouts, sessions, samples | Encrypted Room database on the device | No |
| Screenshots, recordings, exports, session cards | App-private storage | Only when you hand one to your own share sheet |
| Latency probe | One TCP handshake to a host you choose | Only that handshake; no payload |
There is no backend, no account system, no cloud sync, no analytics, no crash reporting and no
advertising SDK. Everything GameCore records — profiles, HUD layouts, sessions, samples — lives in
its own encrypted database on the device, and none of it leaves on its own. The session card is the
one artefact meant to leave, and it leaves the way a screenshot does: drawn on the device, then
handed to whichever app you choose from your own share sheet. GameCore has nowhere of its own to
send it.
One thing touches the network, and it is honest about it on the screen it belongs to:
- **The latency probe.** The single network call GameCore's own code ever makes — a payload-free
TCP handshake to a host you choose, timed and closed the instant it connects. It exists because
you asked for a ping figure, and it can be switched off. No payload, ever, in either direction.
The 3.5 network check reuses this same probe rather than adding one of its own; everything else
it reports — the transport, the Wi-Fi band, the link speed — it reads from Android on the device.
---
## Release history
Older releases, preserved. Expand any version for the full notes.
<details>
<summary><strong>3.17.0 — Tilt training for phones without a gyroscope</strong></summary>
The gyro challenge no longer needs a gyroscope. A phone without one can run it as **Tilt
training** — a real measurement, read from the accelerometer and labelled as tilt.
- **Bank to steer, tip to aim.** Tilt training reads the phone's lean against gravity: bank the
phone left or right in the plane of the screen to steer, and tip the top edge away from you to
aim up. A phone with a gyroscope is unchanged — the gyroscope is still the input.
- **Its records stay apart from the gyro's.** An accelerometer cannot sense turning on the spot —
gravity does not change when the phone rotates about itself — so a tilt run is stored under its
own mode and is never ranked against a gyro record.
- **No sensor, no pretend run.** A device with neither a gyroscope nor an accelerometer gets a
plain explanation in place of a Start button, rather than a run that could not move.
</details>
<details>
<summary><strong>3.16.0 — Themes can be made of glass</strong></summary>
A theme can ask for glass now. A plugin stays data — it never touches the canvas — but it can now
ask the host for a material, and the host draws every frame of it.
- **Themes can be made of glass.** The theme schema is now 4, and a `glass` block asks for a
material rather than a flat colour: a translucent surface, a specular rim along the top edge, a
highlight that drifts slowly down the face, and an iOS-style spring that settles a panel when the
material changes. A theme that declares schema 3 and carries the block is refused, not
downgraded — a glass theme whose transparency silently became a flat fill is not the design its
author wrote.
- **The blur belongs to Android 12.** From API 31 the backdrop behind a panel is blurred as well.
Below that the host still draws the translucency, the rim and the gloss against a sharp
backdrop, and the Control Center says so — "Sharp — this Android version cannot blur one" —
rather than calling it frosted.
- **One slider.** Settings → Glass Control Center holds a single control: how much of the material
the app is wearing, scaling the transparency, the gloss and the blur radius together. The
preview above it is drawn from the exact material the slider points at, through the app's own
renderer, so you can judge it before you let go. The row appears only while a theme with a
material is in force.
- **Animation settings still win.** Settings → Animations → Off, or the system's
remove-animations switch, and the material is drawn at rest — the translucency, the rim and the
gloss stay, the highlight stops and the panels stop settling. A plugin asks for motion; it can
never take it.
Ships with **Liquid Glass GameCore** (`dist/liquidglass/liquidglass.gcplugin`) — rewritten to ask
for the material it is named after, version 1.2.0, and it needs GameCore 3.16 or newer. It is a
downloadable package rather than a bundled theme: drop the `.gcplugin` file into GameCore's
plugin installer. All twenty official plugin packages ship with the release that way, and nine
of them are also bundled in the app.
</details>
<details>
<summary><strong>3.15.0 — Five launcher icons, and a plugin can choose one</strong></summary>
GameCore has five launcher icons, and a plugin can choose one of them.
- **The picker.** Settings → App icon shows the set: **Core**, **Mono**, **Glitch**, **Neon** and
**Redline**. Tapping one changes the icon your launcher shows — a real switch, not a shortcut to
the app, because each icon is its own launcher component and exactly one is enabled at a time.
- **Plugins can ask.** A plugin with the `change_icon` permission ships an `icon.json` naming a
slot; Settings shows which plugin is setting the icon and offers a one-tap Undo back to your own
choice. A plugin that is disabled, uninstalled or suspended loses the icon on the next launch.
- **Plugin artwork, honestly.** Android has no API that lets an app install a bitmap as its launcher
icon, so a plugin's own image cannot become the icon — and this release does not pretend it can.
Artwork is offered as a **pinned home-screen shortcut** instead: you confirm it in the launcher,
and GameCore's own icon is left alone. The picker says so in as many words.
</details>
<details>
<summary><strong>3.14.0 — Themes can move</strong></summary>
Theme plugins can move now. A plugin stays data — it never gets to touch the canvas — but it can
now *ask* the host to distort what it draws, and the host owns every frame of that.
- **Themes can move.** A schema-3 theme may carry a `glitch` block. The app's content is recorded
into a layer and re-drawn as displaced horizontal bands: the frame tears, text and panels
together, with one-pixel fracture lines, ghost copies at the band edges and translucent block
noise. It is a live re-draw, not a colour filter, and it applies to the whole app.
- **It reads the phone.** With `hardwareReactive` on, a device above 85% CPU tears about twice as
often, and one the platform reports as thermally throttled adds a vertical frame-drop on the
first frame of a burst. Off, the effect keeps a fixed rhythm and reads no sensors at all.
- **Animation settings still win.** Settings → Animations → Off, or the system's remove-animations
switch, suppresses every theme effect. A plugin asks for motion; it can never take it.
- **Off by default.** Nothing changes unless you install and select a theme that asks for it.
Every existing theme renders exactly as it did.
Ships with **True Glitch** (`dist/trueglitch/trueglitch.gcplugin`) — cyan and red on near-black,
monospaced, torn.
</details>
<details>
<summary><strong>3.13.0 — One tap, the full panel</strong></summary>
The floating button stops making you tap twice.
- **One tap, the full panel.** Tapping the game button opens the control panel straight away — every
tab, every control — instead of the small quick sheet. The sheet has not gone anywhere: a double
tap on the button opens it, with the same toggles, sliders and session clock.
- **Or keep the sheet first.** Settings → Overlay → Quick sheet has a switch. Off, a tap opens the
sheet first and the panel stays behind its "More" button, exactly as it used to behave.
</details>
<details>
<summary><strong>3.12.0 — The Theme Engine reaches every screen</strong></summary>
The Theme Engine stops being something you visit and becomes the colour the app wears.
- **App-wide colour.** A switch on the calibration screen hands the app's accent to the engine.
On, every screen is tinted by what the device is doing right now — the same ice-blue, violet and
molten crimson you see on the canvas — and a theme plugin keeps everything else it does but
stops choosing the palette. Off, a plugin's colours win exactly as they always have, which is
the default.
- **The engine runs the whole time.** It starts with the app rather than with its settings screen,
so its colour is live everywhere and the first recolour is a transition instead of a jump.
Calibrating still drives it live; leaving the screen no longer stops it.
- **Say who chooses.** The same card prints the accent in force and whether it came from your
settings, the engine, or nothing at all yet — no guessing which layer won.
</details>
<details>
<summary><strong>3.11.0 — A live backdrop from what the device is doing</strong></summary>
A live backdrop painted from what your device is actually doing — and a calibration screen where
every figure it uses is on the table.
- **Theme Engine.** A canvas in Settings → Theme Engine draws a soft ice-blue drifting at half
speed when the device is cool and quiet, turns Aurora Violet once CPU, memory or temperature
cross their thresholds, and goes molten crimson at speed when it is hot or saturated. The colour
and the speed both follow the readings as they change.
- **See the vector.** The calibration screen prints the mode in force, its exact
`rgb(r, g, b)` value and its fluid velocity beside the three readings it was drawn from — CPU,
memory allocation and core temperature, each labelled with what it is.
- **Calibrate against your own thresholds.** Flat sliders stand in for each reading so you can
watch the backdrop answer without loading the device, and the thresholds that decide each mode
are adjustable.
- **Save points.** Keep a calibration you like under a name and recall it later. Save points are
stored on the device and survive a restart.
The engine is built into GameCore, in its own package rather than in the screens that use it. It
is not a `.gcplugin`: those are data GameCore reads, and this one draws, so it ships with the app.
</details>
<details>
<summary><strong>3.8.1 — Eight official plugins and the plugin host</strong></summary>
Eight plugins GameCore built itself — and the host features that let them do real work, all
still under the rule that a plugin is data GameCore reads, never code it runs.
- **Eight official plugins.** A first set you can install from a file: a Nothing-OS-style theme,
an anime-inspired pack, a sensitivity workspace, an automation-history view, a personal
dashboard, a local advisor, a benchmark readout and a dashboard composer. Each ships bundled and
can also be sideloaded as a `.gcplugin` file.
- **Plugin settings you can change.** A plugin can offer its own options — switches, choices and
numbers — and GameCore stores and applies them for you. The controls live in GameCore; the
plugin still runs nothing.
- **Your automation history, shown by a plugin.** A plugin you allow can display the record of
automations GameCore has already run for you. It reads that history only to show it — it is
handed none of your other data and runs no code of its own.
- **Live readouts on plugin screens.** Plugin screens and dashboard widgets can show figures
GameCore already measures — frame rate, temperature and the like — each drawn by GameCore from
data you have allowed.
</details>
<details>
<summary><strong>3.8 — Data-only plugins arrive</strong></summary>
Plugins arrive — and arrive as data, not code. A plugin is a file GameCore reads to restyle
itself and add small readouts; it never runs inside GameCore and never gets a shell, so the
directory can grow without widening what any one install can do.
- **Install a plugin from a file.** Add a plugin from a `.gcplugin` package, see exactly what it
asks for before anything is on, and turn it on yourself. A plugin is data GameCore interprets
with code it already ships — it carries no scripts, no WebView and no native libraries.
- **Permission by permission.** Every plugin lists exactly what it wants and you grant each item
separately; a new plugin arrives switched off with nothing granted, and anything that touches a
device setting needs a second opt-in.
- **Plugin themes.** A plugin can supply colours, corner rounding and UI scale. Unreadable colour
pairs are replaced automatically, and the Plugins screen always draws in GameCore's own theme —
so the way back is never styled by the plugin you are turning off.
- **On-screen plugin modules.** A plugin can add a small readout that sits over your game beside
the Game Pill — frame rate, temperature and the like. It cannot cover the screen, cannot take
touches from the game, and appears only once you grant that plugin the overlay permission; up to
three at a time.
- **One switch to stop them all.** A single toggle on the Plugins screen stops every plugin at
once, whatever each plugin's own switch says.
- **Order the list your way.** Sort installed plugins by name, by date installed, or by version
number — highest first, with `1.10` correctly above `1.9`. The order only changes how the list
is drawn; it changes nothing about which plugins run.
</details>
<details>
<summary><strong>3.7.2 — Dock customisation and mid-session revert</strong></summary>
The floating dock's buttons become yours to arrange, the active profile can be put back one
field at a time without ending play, and an in-game trigger can be placed by touch — plus the fix
for a release-blocking bug where the trigger-placement overlay swallowed every touch except the
game's own pill.
- **Dock customisation.** Rearrange the floating dock's buttons and choose which of them appear,
saved per game — so the dock a game raises holds exactly the controls that game needs.
- **Quick actions on the dock.** Put shortcuts to GameCore's existing controls onto the dock for
one-tap access without opening the full panel; a quick action runs through the same handler the
panel's own tile does and invents no new capability.
- **Revert settings mid-session.** A card on Home lists exactly what the active profile changed on
your device and puts any of it back without ending play — the same reading-paired restore a
profile does on exit, offered one field at a time while the game runs.
- **In-game point-trigger placement.** Place a volume-key tap target by touching the spot on
screen, from inside the game and without opening the app. It is saved per game, and the tap it
later fires is the one synthetic input the app can send (see below), gated on Shizuku.
</details>
<details>
<summary><strong>3.7.0 — Floating dock, screen extraction and touch tools</strong></summary>
A second overlay surface and four stand-alone tools, each holding the app's one rule: what it
cannot measure or do, it says plainly rather than faking.
- **Floating Dock.** A small draggable dock over your game with its own quick control panel; it
snaps to the screen edge, is turned on from Settings, and sits alongside the existing floating
button rather than replacing it.
- **Screen Extraction.** Capture the current screen, crop a region, then save or share the
result — drawn and cropped on the device and handed out through the same one-shot share grant
every other artefact uses.
- **Touch Sampling Monitor.** A live read of the device's real touch input rate. Where the
hardware will not report it, it reads **Unavailable** rather than a plausible number — the rule
every reading in the app follows.
- **Volume Button Point Trigger.** Map a volume key to tap a saved screen point for a game —
single, double or hold. It needs the optional accessibility service, so the key reaches GameCore
from inside another app, and Shizuku, so the tap can be injected; without either, the control
says so rather than pretending to work.
- **What's New.** Shows the features a release introduced the first time you open the app after
updating, and lets you browse every past update any time from Settings.
</details>
<details>
<summary><strong>3.6.1 — Network stability, performance HUD and custom overlays</strong></summary>
Three leaf screens, each turned on or off from Settings, and each holding the app's one rule: a
figure it cannot measure is shown as "Unavailable", with the reason, never as a zero.
- **Network Stability Mode.** A live read of the connection you play on — transport, signal,
estimated link speed, device-wide download and upload, and whether the system calls it metered —
with a stability window built from TCP-handshake bursts. It reports ping (median handshake time)
and jitter (the variation between consecutive probes, the honest stand-in for the packet-loss
figure it will not fake), and how many probes in the window completed. Where Android allows it, it
also keeps the device from switching networks mid-game.
- **Advanced Performance HUD.** A customisable heads-up display over FPS, CPU, GPU where the device
exposes a load node, RAM, temperature, battery, ping and session time, as a compact line or an
expanded card. Any stat the device will not report reads "Unavailable" rather than a made-up value.
- **Custom Overlay Modules.** Turn individual overlay modules on and off, drag them to position,
resize the ones that support it, and choose exactly which information each shows — saved as an
overlay layout per game that a profile can raise by name.
</details>
<details>
<summary><strong>3.6 — Instant Replay</strong></summary>
The feature people ask for by name — keep the last few seconds of play, and decide to save them
after they have already happened.
- **Instant Replay.** A rolling video buffer that always holds the most recent stretch of the
game — 15, 30, 60 or 120 seconds, 30 by default — so a moment you did not know you would want is
already recorded by the time you reach for it. When you save, the buffered window is written out
as a single clip and nothing outside it is; the buffer itself lives in the app's own private cache
and is trimmed back to the length you chose as it rolls, so it never grows without bound. It is
**off until you turn it on**, set per game like every other profile field, and the first time you
enable it a one-time notice states the three things worth knowing first: the buffer records the
whole time the game is open, everything stays on this device, and **this version is video only** —
it holds no microphone permission and captures no audio, on purpose, because a "record audio"
switch that produced a silent track on half the games it was used with is exactly the kind of
control this app refuses to ship. It reuses the same screen-capture consent the recorder already
asks for, so it adds **no new permission**. An in-game chip shows when it is armed, and a save
control sits in the overlay while the game runs.
</details>
<details>
<summary><strong>3.5.4 — Config editor and per-game resolution override</strong></summary>
Two features about reaching past the settings a game exposes — to the files behind them, and to
the resolution it renders at.
- **A config editor.** Browse a game's own configuration files and edit one in place, with the
whole change shown as a line-by-line diff before a single byte is written — nothing is saved
until you confirm exactly what changed. The write is whole-file and atomic, so a config is never
left half-applied. The first time you edit a game's config its original is backed up
automatically; you can keep named checkpoints and restore any of them later, and if the game has
been updated since that backup the editor says so, because an edit made against a config the
update already replaced is an edit against a stale file. It reaches those files through the same
elevated shell the rest of the app uses, so it needs Shizuku — without it the screen says so and
points at the setup — and a path outside the selected game's own config is refused rather than
opened. A one-time notice explains the feature before the first edit, and can be shown again.
- **Resolution override.** A per-game render resolution, set as a scale of the display's native
size and applied through `wm size` when the game comes to the front — then cleared and the
display put back exactly as it was when the game leaves, the same restore rule every other
profile field follows. Lowering it can lift frame rate on a GPU-bound game at the cost of
sharpness; it is per game, off unless you set it, and it asks once for confirmation the first
time because it reshapes the whole display while the game runs. An in-game status chip shows when
an override is active, so a changed resolution is never a mystery. Needs Shizuku, like every
display-size write.
Also since 3.5, and until now in no release:
- **A pinned magnifier.** A loupe overlay that shows the centre of the screen enlarged — a
crop-and-scale of a single capture frame, parked in a corner with its geometry chosen so it can
never fall over the region it magnifies and mirror itself. There is no detection, no tracking and
no aim logic: it enlarges pixels and nothing else. It uses the same screen-capture permission the
other overlays do.
- **Panel macros.** A named, ordered set of overlay actions you already have, fired in sequence by
one tap. A macro invents no new capability — every step runs through the same handler the panel's
own tiles use — so all it adds is the order and the count. They live as an editable, reorderable
config list, not a database table.
- **Config backup and restore.** Export every configuration store — profiles, presets, overlay
layouts, macros — to one file you can keep or move to another install, and restore it later. It
touches configuration only: your recorded sessions and Aim Lab runs are never written into the
file and a restore can never overwrite them, and everything a restored file carries is validated
before it reaches a store.
- **Profile transfer.** Share one or more game profiles as a single file, presets and all, and
import them elsewhere — recreating what is missing and matching-then-remapping what already
exists, so an import never silently duplicates a preset or clobbers one you had.
- **Profile suggestions.** A starting profile derived only from what a game was actually measured
doing, with a plain sentence behind every field it fills. A dimension the sessions did not measure
enough to speak to is left alone; a game with thin or contradictory history gets no suggestion at
all rather than a guess dressed as advice. It only drafts — nothing is saved or applied until you
open it in the editor.
- **Per-sample export.** Alongside the one-row-per-session export, write one row per *sample* — the
raw time series a graph is drawn from — as CSV or JSON. It keeps the same honesty rule the
aggregate export follows: blank, never zero, for a reading a sample never took.
</details>
<details>
<summary><strong>3.5 — Setup wizard, thermal auto-downshift and network check</strong></summary>
Four features about the app setting itself up, and then looking after the session on its own.
- **A first-run setup wizard, and a Setup health screen.** A new install now walks through what
GameCore needs — which features you want, and only the permissions those features actually use —
instead of leaving you on an empty Home screen to work it out by trial. Nothing is granted on your
behalf and no step is required: skip all of it and the app still runs, minus whatever you skipped.
Afterwards, **Settings › Setup health** lists every item with its real state — Ready, Not set up,
or Unavailable with the reason Android gave — a Fix button that opens the exact system page, and a
re-check when you come back from it. Upgrading never re-runs the wizard over your existing
profiles; it offers a dismissible Home card instead, and only when something genuinely needs
attention.
- **Thermal auto-downshift.** Off by default, per profile. When the device holds a temperature you
choose, or the thermal status Android reports crosses the floor you set, the refresh rate steps
down — and steps back up once it has stayed cool long enough. The hysteresis, the sustain window
in each direction and the minimum time between changes are all yours to set, so it cannot sit
there oscillating. The session records how many times it stepped down and the lowest rate it
reached.
- **Network check.** Off by default, per profile. Before a launch it can warn you about the
connection you are about to play on; during a session it can alert you when the connection turns
poor — high latency, jitter, or probes that stop coming back at all. It still refuses to call any
of that packet loss, because a TCP handshake that times out is not a dropped packet. It reads the
transport, the Wi-Fi band and the link speed — **never the network name, never the BSSID, and it
asks for no location permission**, which is what Android would demand before handing those over.
Anything Android will not report shows as Unavailable with the reason.
- **Keep full performance under battery saver.** Off by default, per profile. Battery saver caps
the refresh rate and the governor on most builds; with this on, a profile holds full performance
for that game anyway, puts the saver's own behaviour back when the game exits, and notices — and
records — when the system switched the saver back on behind it. No new access of any kind: it
goes through the same elevated shell the refresh-rate setting already uses.
Also since 3.2, and until now in no release:
- **A redesigned in-game overlay.** The floating button has real states — dimmed when idle, an
orange or red dot when the shared thermal classifier says hot or critical — three sizes and four
corner snaps, and it keeps its place per orientation. Tapping it opens a **quick sheet**: a narrow
panel on the screen edge nearest the button, holding up to six toggles you pin yourself, the
brightness and volume sliders and the session clock, so the things you reach for mid-game no
longer need the full panel and most of the game stays visible. The full panel is still there,
behind **More** or an optional double tap, now divided into Display, Overlays, Capture and Session
tabs with a hold-to-confirm End session (the older split-across-both-edges layout keeps its
previous design). The stats pill comes as a compact single line or a detailed card, and it stops
sampling the moment nothing is looking at it — including when the screen goes off, where it used
to keep ticking.
- **Themes.** Follow system, Dark, Light or **AMOLED black** — a true `#000000` background that
switches OLED pixels off rather than dimming them — with eight accent colours or one you pick
yourself, applied across the app and the overlay windows alike.
</details>
<details>
<summary><strong>3.2 — Aim Lab fire modes and on-screen controls</strong></summary>
Two additions to the Aim Lab, both about the weapon and the way you hold the phone.
- **Fire modes.** A weapon now discharges in one of three ways — **single**, a fixed **burst**, or
full **auto** — and the training loop honours it: a burst walks up the recoil pattern at the real
fire-rate cadence rather than landing all at once, and the magazine and reload gate every mode the
same. The weapon editor picks the mode and, for burst, the round count; a built-in Burst Carbine
ships so the mode is there to try without building one. Older saved weapons read back as auto —
exactly what they already did — so nothing changes under you.
- **On-screen controls on every mode.** The saved control layouts (HUDs) you build in the editor can
now be drawn over the arena during a run from any mode's setup screen, not just some — pick one, or
leave it on "None" for a clean view. The choice is per run and never forced.
Also from 3.1: the Aim Lab runs in **landscape** with a real horizontal field-of-view setting, the
gyro axes remap correctly per screen rotation, and control layouts are stored per orientation.
</details>
<details>
<summary><strong>3.0 — The offline 3D Aim Lab</strong></summary>
An offline **Aim Lab** — a first-person 3D training arena, built inside GameCore and held to the
same rule as everything else: nothing it reports is invented.
- **A real first-person 3D room, not a flat target board.** You stand inside an enclosed arena —
checkered floor, grey walls and ceiling, light depth fog — with a low-poly weapon held at the
bottom-right and glossy spheres floating at varied distances and angles. The crosshair is fixed
dead centre; you look by dragging, and a shot is the ray straight out of the crosshair, hit-tested
against the spheres in 3D. Rendered with OpenGL ES 2.0 and nothing else: no game engine, no
third-party 3D library, no model or texture files — the room, the spheres and every weapon are
built procedurally in code, and the shaders are plain source strings.
- **Seven modes, all in the 3D view.** Flick, Tracking, Reaction, Gyro, Recoil, Movement and a Free
Practice sandbox. Aim error is measured in **degrees**, not pixels, so a result means the same on
any screen. Recoil kicks the camera along the weapon's pattern and scores how well you pull it
back; Movement walks you through the room and scatters shots taken on the move; Gyro turns the
view from the real gyroscope. A phone with no gyroscope but an accelerometer runs the gyro
challenge as **Tilt training** — bank the phone left or right in the plane of the screen to steer
and tip the top edge away from you to aim up — read from the accelerometer, and filed under its
own mode so a tilt run is never ranked against a gyro record.
- **Your gear drives it.** The weapon editor, sensitivity lab, control/HUD editor, statistics,
personal records, session history and JSON/CSV export/import all carry over — and a weapon's fire
rate, magazine, reload, spread and movement penalty are now actually felt in a run.
- **Old training data is kept, and kept separate.** Sessions recorded by the previous flat trainer
stay in your history, labelled as legacy, and are never ranked against the new 3D scores, because
the two are measured differently. The database upgrade is additive — no history is dropped.
- **It stays out of the way when you leave.** The 3D view renders only while a run is on screen; the
moment you leave or background the app, rendering stops, the motion sensor is released and the run is
cancelled. If a device cannot run OpenGL ES 2.0 the arena shows a plain "3D view unavailable"
panel instead of crashing.
The Aim Lab is off by default behind a master switch, and turning it off is a real disable, not a
hidden screen.
</details>
<details>
<summary><strong>2.0 — A second panel shape and in-game controls</strong></summary>
Five additions, all of them about reaching a setting without putting the game down.
- **A second shape for the control panel.** It can now split into two plates pinned to opposite
screen edges — readouts on one, controls on the other, the game still visible between them, full
screen height — instead of one plate below the button. Centred stays the default, because a panel
you have already learned should not rearrange itself on an update. A **Layout** tile inside the
panel switches between the two on one tap, so trying the other shape costs nothing and neither
does going back.
- **Refresh rate from the panel.** Every rate this display reports, as chips under the tile grid,
plus the way back off a pinned one. The tile lights only from a change GameCore made and
confirmed: the platform is entitled to drop a pinned panel to 60 Hz on a static screen, so a low
reading is not evidence the pin failed and a high one is not evidence it holds. A rate that was
written but could not be read back says so rather than claiming it took.
- **Crosshair quick-select.** Hold the crosshair tile for the active crosshair's design and colour
without opening a screen. It offers the built-in colours plus the ones you already mixed
somewhere with more room — no HSV square, because that is a two-handed control and this window is
open over a game.
- **An eleventh crosshair design, and more colours.** A box: a square outline, for framing a target
rather than marking a point.
- **CPU core affinity presets, marked experimental.** A per-profile choice of which cores a game's
main process may run on — performance cores only, or all cores minus one efficiency core. The
app says what this is worth in the same words every time: it may reduce stutter by controlling
which cores the game runs on, it does **not** make the device faster, and depending on the game
it can do more harm than good. Needs Shizuku. The mask the process was found on is recorded
before anything changes and written back when the game exits.
</details>
<details>
<summary><strong>1.3 — Game storage, connection history and a shareable card</strong></summary>
Three additions, and four reported bugs fixed.
- **Game storage.** A screen that measures what every game is holding in cache, and clears one
game's shared-storage cache on a tap. It reads the size, deletes, waits, reads again, and reports
**the difference between the two readings** rather than an exit code — so a clear that freed
nothing says nothing was freed, instead of claiming a number it did not measure. It never touches
a save, a login or a downloaded asset. [Details](#game-storage).
- **What the connection did.** Every session now carries a latency log: probes that completed,
probes that did not, the worst reply, jitter, the longest run of failures, and one of five
verdicts with the figures behind it in a sentence. No packet-loss percentage anywhere, because a
refused TCP handshake is not a dropped packet. [Details](#sessions).
- **A card you can share.** A finished session renders to a 1080×1260 image and goes to your own
share sheet. A reading this device could not take is a dash on that card and never a zero, an
interrupted recording says its length is a lower bound, and the sample count travels with the
averages. Drawn on the device; nothing is uploaded. [Details](#sessions).
The bugs, all four found in use rather than in a test: a crosshair that drew the dot preset whatever
you picked — and the HUD, which had the identical bug, because a layout you arranged is not "any
layout"; a profile edited after leaving a game that did nothing until GameCore was restarted; the
resolution card stuck on "loading" after quitting a game; and a Do Not Disturb popup that put DND
back on after you had turned it off by hand. The last one is now impossible by construction:
GameCore restores a device setting only when it made the write and nothing outside GameCore has
changed it since.
</details>
---
## License
MIT — see [LICENSE](LICENSE).
<p align="center"><sub>Built with Kotlin and Jetpack Compose. No trackers. No telemetry. Just readings.</sub></p>