← Dev Log
BallOrcs8 min read

A permanent name, a PAUSE button, and 129 crashes that were one bug

BallOrcs got its permanent phone name, touchscreen prompts that name the real buttons, and a PAUSE button. Plus 129 crashes that were one bug.

In plain words

BallOrcs now has a permanent name — com.helixplaygames.ballorcs — the one it will carry on a phone forever, because that name can never be changed once a game is published. Around it, everything a store actually checks got nailed down: which version of Android it targets, a version number that climbs by itself with every change, and the signing key that proves a build came from us.

Playing on a touchscreen got noticeably less silly. The game used to tell phone players to "Roll with W" and press Esc for options — keys they do not have — and there was no way to pause at all. Prompts now name the buttons actually on screen, those buttons have words on them, and there is a PAUSE button in the corner.

We also read every orb and hazard through simulated colour blindness, and they passed.

The fire level's oil slick, run through the colour-blindness check added this week: the shot as shipped, then protanopia, deuteranopia, tritanopia, and full greyscale. The pass asks one question — can you still tell an orb from a hazard once hue stops helping? The answer was yes, because everything differs in shape as well as colour.

Under the hood

  • 129 crash reports, one non-bug (edcdba0, #131). The first CI soak that could actually reach the crash backend produced 129 separate issues from a single engine quirk: Area3D's teardown during a scene reload disconnects tree_entered/tree_exiting from bodies whose connections are already gone, so every trigger volume a respawning bot overlaps spams "Attempt to disconnect a nonexistent connection". The same spam is why the perf oracle had flagged headless tick_ms +75% — with crash reporting live, every one of those errors was building and shipping an event mid-tick. Fixed with a guard on all four disconnect pairs in area_3d.cpp, carried as build/patches/godot-area3d-disconnect-guard.patch rather than a forked engine, since the submodule tracks upstream. Two details make that safe: apply_engine_patches() is idempotent and dies if a patch fits neither forward nor reverse, so a moved engine pin can never silently drop the fix; and engine_rev() folds a digest of build/patches/* into the cache stamp, without which cached CI editors would skip the rebuild and stay quietly unpatched.
  • Android became publishable, in three parts (#129). Package id: com.helixplaygames.ballorcs, backed by the domain registered this week — permanent once published, so the preset carries a warning comment and the build asserts it (dc753aa). versionCode: git rev-list --count HEAD, which needs no counter file and correctly collides when the same commit is built twice, since the same build should never be uploaded twice. It is computed on the host side of the container re-entry, because inside the container the checkout is dubious-ownership and git refuses — failing there would have silently shipped versionCode=1 (8db3342). Signing: release keys arrive as protected CI variables, and the export refuses to finish unless apksigner --print-certs shows the APK is not debug-signed and is signed by the HelixPlayGames key (53442b2). Separately, target SDK 36 is now pinned rather than inherited, with aapt2 badging asserting it after export (d0e5a8b) — the investigation found current builds already shipped 36 by engine default, so this buys the guarantee, not the number.
  • The Android build broke on a check that had only ever passed by luck (c9c3ad1). The 4.7.2 engine bump went green on linux, windows, web, smoke and verify, and failed only on build:android with "A valid Java SDK path is required". Godot names its editor-settings file by major.minor (editor_settings-4.7.tres); the script wrote ${GODOT_VERSION%%-*}.tres, which strips only the -stable suffix. At 4.7-stable that yielded 4.7 by coincidence and had always matched; at 4.7.2-stable it wrote editor_settings-4.7.2.tres and the editor went on reading the default, SDK-less file. Now derived explicitly with cut -d. -f1,2.
  • Touch players were being read instructions for hardware they do not have (1717923, #124). prompt_text() gains a touch tier — pad glyph if a pad is connected (a pad on a phone outranks touch), else the overlay's control names, else the keyboard binding. Those names deliberately avoid colour words: "the blue button" would undo the colour-safety stance below. The two colour-only circles are now captioned JUMP and ABILITY, so the prompts have something visible to point at. That change closed with a stated gap — touch had no route to Options at all — which became feffd08 (#125): a safe-area-aware PAUSE button bound to the same pause action every other device uses. The subtle half is surviving the pause it causes: TouchControls becomes PROCESS_MODE_ALWAYS, because inherited PAUSABLE would freeze the overlay the instant its own button fired, release events would stop arriving, and a joystick held at pause time would latch its move actions.
  • A parse error had killed every dialog in the game (fe16974). The pad-B skip used var pad_skip := on an expression with no inferable type — a parse error, so dialog_box.gd failed to compile and story_beats went down with it. Eight rounds of hands-on feel testing missed it because this machine's save has every story beat already seen, so no dialog ever tried to open; CI, on a fresh profile, waited four hours for a prologue that could not exist. The fix is one explicit type, plus the lesson: verify_dialog grew a 120 s watchdog, because a stage that cannot complete should be a red result, never a job timeout.
  • Colour safety, judged by a human and re-runnable by a script (101b308, #108). The audience skews 8–16 and roughly 1 in 12 boys has some red–green deficiency, which puts a fire level built on reds and oranges closer to correctness than to polish. tools/cvd_sheets.py renders each camera anchor as original | protanopia | deuteranopia | tritanopia | greyscale, using Machado severity-1.0 matrices applied in linear RGB — applying them gamma-encoded understates the loss. Verdict: pass, every orb and hazard type differing in silhouette and not only hue, with audio already double-coding the important interactions and rumble adding a third channel.
  • The camera stopped jittering on displays we do not own (8221f3b, #70). Physics interpolation was off — the engine default, never set by the project — so the 60 Hz-ticked ball rendered at raw tick positions while the camera moved per frame. Invisible on the 60 Hz reference laptop, guaranteed micro-jitter on anything faster. Turning it on has two consequences, both handled: teleports must snap, not smear (respawn happens inside a physics tick, and an un-reset one measured a 312 m smear across the map for a single frame), and the camera now tracks get_global_transform_interpolated(), so it aims at the ball players actually see. The follow lerp became 1 - exp(-speed * delta), giving identical response at every frame rate, where the old linear factor smoothed ~8% harder at 30 fps than at 60.
  • The perf oracle was measuring two populations, neither of which was playing the game (35ac72e, #117). Bots run headless (nothing drawn); the camera probe takes no input (the ball never moves). So the three particle emitters added the week before measured zero draw calls — they sat at emitting = false for the probe's entire run, and "idle emitters cost nothing" reads exactly like "these effects are free". A third population, rendered_play, now runs one windowed goal bot for 30 simulated seconds. It is deliberately report-only: no baseline exists for it and no run-to-run noise has been measured, and last week's lesson was that a gate calibrated on nothing fires on everything.
  • The 60 fps floor, measured rather than asserted (e2ff841, #79). Windowed goal-bot runs across L1–L3 on both renderers, on the reference laptop. Forward+ holds min=60 on all three levels once the pipeline cache is warm. The gl_compatibility fallback holds everywhere except one deterministic dropped frame per lap on L3 — same route position in independent runs, coinciding with the run's highest draw-call window at the spawn vista, where GL on an Iris Xe grazes 16.7 ms. Recorded as a known boundary rather than "fixed", because the fix would be vista work the shipped renderer does not need.
  • The web build finally met a browser (fa80336, #86). It had been built every week and nothing ever ran it. A weekly job now serves dist/web with the headers the loader expects, rewrites the page's own config as served so the in-page harness boots a goal bot, and watches the console over the Chrome DevTools Protocol. Three-way verdict on purpose — pass / fail / harness-missing — so a stale pack is reported as a boot failure instead of hanging the job. The bot wins three times inside headless chromium.
  • Two more things that could not disagree quietly. The build now dies when a submodule pointer disagrees with versions.lock (273a999): the lock is a record, the gitlink is what actually gets compiled, and nothing compared them — one merge had the lock at 4.7.2 and the gitlink still at 4.7, compiled a 4.7 editor, installed 4.7.2 templates, and only noticed 18 minutes in at the export step. And every shipped component definition must now resolve to a live mesh after registration (eaa1e91, #119), so "present in data, absent in geometry" cannot ship quietly whichever half breaks.
  • Where the game is going, on paper (99f3ab5, #122). A 20-level plan — five zones of four, one ability unlock per zone, the Hollow King as a colour-drain villain and restoration as the win condition — with the existing three levels becoming hub training grounds. Explicitly a draft for markup, not implementation, and the owner has since marked it up: the hero is the BallOrc, and the goal is to unite the tribes.
  • Housekeeping. The engine pin moved up to 4.7.2-stable (b617a6e); engine builds are serialised with a resource_group so a second runner slot can pick up cheap jobs without two multi-hour compiles racing into an OOM kill (13b05a2); display suites run under --fixed-fps 60, after a 4-thread build host emitted 141 of the 200 pixels a threshold calibrated on an 8-thread box demanded (4205e18); and smoke:windows skips on gitlab.com, where no Windows runner exists (0fa077b).
#android#touch#testing#engine#ci#accessibility