• C++ 53.6%
  • Objective-C++ 15.6%
  • Python 11%
  • Kotlin 6.4%
  • CMake 4.6%
  • Other 8.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Casuallynoted a52250a360
All checks were successful
Android / build (push) Successful in 7m45s
server image / test (push) Successful in 58s
server image / publish (push) Successful in 27s
Lock the app to portrait on iPad as well as iPhone
An iPad would rotate and try to lay the game out sideways. There is no
landscape composition to fall back to: the beach is drawn for a 320x480
viewport, the HUD is anchored to its top and bottom, and every nib is
320 points wide.

Info.plist already said portrait. iPad reads the ~ipad variant of the key
when it is there and falls back to the plain one when it is not, and the
fallback was evidently not enough, so both are spelled out. The one that
actually binds is the new
-application:supportedInterfaceOrientationsForWindow:, which overrides the
plist rather than joining it, and which answers for the whole app -- a
UINavigationController answers for itself rather than for the page inside
it, and every parchment screen is presented in one, so locking them
individually would mean a subclass per stack.

Confirmed on an iPad Pro 11-inch simulator: UIKit logs the scene's
orientation preference as `none -> ( Pu )`, portrait upright and nothing
else, where it previously had no preference at all.

Noted while confirming it, and not addressed here: UIKit now raises a
runtime issue saying UIRequiresFullScreen "will soon be ignored" and that
"support for all orientations will soon be required". That is a real
future problem for a game with one layout, and it needs a decision rather
than a key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 15:56:34 -04:00
.forgejo/workflows Point released builds at the server, and settle the winding 2026-08-08 11:42:34 -04:00
admin Say why push is off, and accept the variable name people reach for 2026-08-08 12:15:11 -04:00
client Lock the app to portrait on iPad as well as iPhone 2026-08-08 15:56:34 -04:00
DistantShore.app Track the original bundle; point deployment at casually.cat 2026-08-05 23:38:46 -04:00
re Point released builds at the server, and settle the winding 2026-08-08 11:42:34 -04:00
scripts Massive push 2026-08-08 00:26:38 -04:00
server Say why push is off, and accept the variable name people reach for 2026-08-08 12:15:11 -04:00
.gitignore Stop tracking the generated Xcode project 2026-08-08 03:42:46 -04:00
AUDIT.md Point released builds at the server, and settle the winding 2026-08-08 11:42:34 -04:00
README.md Keeping things moving 2026-08-06 16:55:15 -04:00
release.sh Add a push diagnostic to the admin app 2026-08-08 03:29:38 -04:00

Distant Shore

A preservation project for Distant Shore (Blimp Pilots, 2009), a networked iPhone game whose servers have long since gone dark. The goal is a client that runs on current phones and a server you can host yourself.

What is here

Path What it is
DistantShore.app/ The original 2009 bundle, the source of truth
re/tools/ Format readers and the asset converter
re/docs/ Recovered specifications — see below
server/ The backend: legacy XML protocol + a modern JSON API
.forgejo/workflows/ CI that publishes the server image

This is a private, self-hosted repository. The original bundle is tracked because it is the irreplaceable input everything else derives from.

Generated directories

re/extracted/, re/assets/, re/decomp/, and re/ghidra/ are gitignored because they are mechanically produced from the bundle and would churn on every tool change. Regenerate them:

python3 re/tools/wad.py DistantShore.app/resources.wad re/extracted
python3 re/tools/convert_assets.py DistantShore.app/resources.wad re/assets

The decompiler output comes from re/tools/DumpDecomp.java run under Ghidra headless; it takes about 30 seconds for all 1,237 functions.

The client is written fresh against the specs in re/docs/ rather than translated from decompiler output, because the original is armv6 with fixed-function OpenGL ES 1.1 — not a usable porting base for arm64 and Metal.

Ghidra renders every literal-pool constant as a bare DAT_xxxxxxxx, which is why a lot of early work in this repository described mechanisms correctly but got their numbers wrong. re/tools/litpool.py dereferences those slots against the shipped Mach-O — strings, selectors, floats, arrays — so a constant can be read rather than inferred. Use it for anything numeric.

Period screenshots

Ignored Files/OriginalAppImages/ holds screenshots of the original running, at its native 320x480, plus the 512x512 App Store icon. The directory is gitignored, so keep a copy safe — these are primary evidence and nothing in the repository can regenerate them.

They settle things the binary cannot. The in-world HUD is three unlabelled icons and no text at all (an info button top-left, the item drawer bottom-left, the bungalow/teleport bottom-right); the item drawer is 4 slots across by 2 down per tab, each message showing a scroll icon and a truncated preview; the four tab pages sit on parchment under a standard navigation bar with a "Return" button. The Help page also states the loop in the developers' own words — "Tap the sand to walk the shore… Tap shells to pick them up… Collect 5 shells to earn an empty bottle" — which independently confirms the mechanics recovered from the binary.

They also contradict one of them: see the pier001 entry under Not recovered in props.md.

The app icon is built from the App Store artwork by client/ios/make-icon.py.

The specifications

Document Covers
protocol.md The six endpoints, XML shapes, parser strictness, polling
formats.md WAD, mesh, texture, dsi_bounds, CgBI PNG
gameplay.md Inventory, items, shell economy, collision, ocean, pathfinding
props.md World shape, the walkability lattice, the prop database, placement, shells
delivery.md How a message becomes a bottle on the sand or a letter in the turtle's mouth
audio.md The sound table, ambient schedulers, footsteps, spatialisation
ui.md The HUD atlas and its sprite table, the shell gauge, the tab pages, the terms screen
atmosphere.md Time of day, lighting gradients, shadows, bird flocks
particles.md The emitter template database, spawning and integration, the sprites

AUDIT.md records where this repository's own earlier claims were wrong, what has been corrected, and what is still missing. It is worth reading before trusting any part of the client as faithful.

Server

Serves two front ends over one shared data model, so both see the same ocean:

  • /bc/*.php — the original 2009 XML protocol, reproduced from the shipped binary. The original client can talk to this unmodified.
  • /api/v1/* — JSON for the new client.
cd server
docker compose up --build        # http://localhost:8000, docs at /docs

Full detail, configuration, and deployment notes: server/README.md.

Deploying

Pushes to main that touch server/** run the test suite and, if it passes, publish a multi-arch (amd64 + arm64) image to the container registry on git.casually.cat.

To deploy on the NAS, copy server/docker-compose.prod.yml and server/.env.example (as .env) to the host — it pulls the published image, so the host never needs this repo:

docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d

The server is served at https://distantshore.casually.cat. Set HOST_PORT=127.0.0.1:8000 in .env if a reverse proxy terminates TLS in front of it, so the container is not also exposed directly on the LAN.

CI needs one repository secret, REGISTRY_TOKEN — a Forgejo access token with write:package. The runs-on: docker label must match how your runner is registered.

Status

The server reproduces all six original endpoints and the JSON API (44 tests). The client core is 232 tests, clean under ASan/UBSan.

Simulation — largely recovered. The world is the original's paged strip of nine tiles, each a 15x15 lattice of 2/3-unit cells; props come from the recovered 35-entry database with its real rarity, scale and density; movement is A* with the original's cost model; shells are collected by walking onto them. Message delivery, the 400-second day/night clock and the ES 1.1 shading model are in. Feel constants (walk speed 1.25, 48-degree camera, 0.4 footstep stride, 20-second poll) are read from the binary rather than guessed.

Presentation — partly. The terms screen and launch image are restored from the nib and the bundle. The HUD draws the shell gauge and mail indicator from the original atlas.

Still missing, and visible: the item drawer (so Write/Messages are placeholder buttons), the bungalow and info buttons, the four tab pages, shadows, bird flocks, particles, and every pickup and throw animation. AUDIT.md tracks the rest.

Reverse engineering notes

Recovered with Ghidra from the shipped armv6 binary, which ships decrypted (cryptid 0) and retains its symbol names, so the analysis reads in terms of real classes rather than addresses.

Findings are validated rather than assumed. All 56 meshes parse with every declared stride equal to the sum of its components and zero of 19,947 indices out of range; all 108 textures satisfy length == 60 + w*h*channels exactly, and decode to imagery with a mean adjacent-pixel delta of 2.24 against roughly 85 for noise.

The sharp edge in the mesh format, documented in re/docs/formats.md: BpMeshTemplate offsets are file-relative but BpPrimBuffer offsets are relative to the prim buffer's own base. Mixing them yields plausible-looking garbage rather than an obvious failure.

About

Reimplementation of Distant Shore, an early networked iOS game.

Releases

Topics

Metadata

137 MiB

Languages

  • C++ 53.6%
  • Objective-C++ 15.6%
  • Python 11%
  • Kotlin 6.4%
  • CMake 4.6%
  • Other 8.7%