Lunar Porter moves into a directory of its own

The first application with things of its own, and the first customer for
all three rungs before it at once.

  /Apps/Lander                      a launcher
  /Packages/app.Lander/Lander.sbx   the program
  /Packages/app.Lander/splash.tune  its tune

Typing "Lander" starts the launcher, which needs no new rule: the shell
runs what it reads, so a launcher beginning "#!" and Say.sbx beginning
"SBEX" go down the same road and neither the shell nor the person has to
know which kind of thing they started. It passes on what it was told with
$args, and says #quiet, because a launcher is machinery and not
narration. The game then asks where it came from and joins its tune's
name to that, so nothing anywhere names /splash.tune.

Every recorded Lander test still types just "Lander" and knows nothing
about any of this, which is exactly the claim.

---- Why the directory is not in /Apps ----

A launcher and a directory of the same name cannot both be there; SBFS
refuses the second. And /Apps is the directory the shell walks for every
word it does not know and Tab walks for every first word, so doubling
what is in it is a cost on the path that runs most.

The "app." goes in FRONT rather than behind because Tab matches the start
of a name: a prefix is a namespace and a suffix is a collision. Copy.app
beside Copy.sbx makes "Copy" and Tab complete to the shared "Copy." and
hand you a broken word.

The launcher names drive 0, so a game started from a disk of your own is
looked for where the game is rather than where you are - its lines run on
your disk, which is what makes everything else in a script work.

/lander.state stays at the root. A saved position belongs to whoever
saved it and is found where they are standing; a tune belongs to the
program and is found beside it. That pair is the whole distinction.

---- And what it cost ----

Two hundred thousand cycles, about twelve frames, between the machine
starting and the game drawing: a script opened, a deeper path walked. Six
video captures moved out by that much. The drift bar needed something
else - its pad is counted from the MACHINE starting rather than the game,
so the burn is now twelve frames shorter from the game's point of view,
and the recording holds the button longer instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-06 15:55:19 -04:00
co-authored by Claude Opus 5
parent 2defbb49e2
commit 94bdf71356
16 changed files with 210 additions and 65 deletions
+2 -3
View File
@@ -26,13 +26,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -51,7 +50,7 @@ args.script 64
pass.script 20
holds.script 87
crossed.txt 560
38 files, 1 directory
36 files, 2 directories
> exit
halted
Execution halted.
+2 -3
View File
@@ -16,13 +16,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -40,7 +39,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> drive 1
> dir
other.txt 28
+2 -3
View File
@@ -33,13 +33,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -57,7 +56,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> exit
halted
Execution halted.
+2 -3
View File
@@ -23,13 +23,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -47,7 +46,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> exit
halted
Execution halted.
+2 -3
View File
@@ -23,13 +23,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -47,7 +46,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> exit
halted
Execution halted.
+2 -3
View File
@@ -109,13 +109,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -133,7 +132,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> exit
halted
Execution halted.
+2 -3
View File
@@ -28,13 +28,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -52,7 +51,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> exit
halted
Execution halted.
+2 -3
View File
@@ -16,13 +16,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -40,7 +39,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> load
load what?
> load nosuch.sbx
+2 -3
View File
@@ -14,13 +14,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -38,7 +37,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> load Say.sbx
loaded, starting at 5000
> run the disk took its time
+2 -3
View File
@@ -23,13 +23,12 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 11260
Packages <dir>
Pad.sbx 264
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
splash.tune 210
tune.sbx 318
Play.sbx 2526
notes.txt 21
@@ -47,7 +46,7 @@ aloud.script 59
args.script 64
pass.script 20
holds.script 87
37 files, 1 directory
35 files, 2 directories
> exit
halted
Execution halted.
+22 -4
View File
@@ -153,12 +153,22 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Depth.asm" -o "$WORK/Depth.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Depth.sbx" >/dev/null
# Lander.sbx is Lunar Porter, and the biggest program on the disk. It takes the screen, the
# mode and the map, so it is also the one that says a program can have all three back.
# ---- Lunar Porter, which is the first application with a directory of its own ----
#
# Lander.sbx is the biggest program on the disk. It takes the screen, the mode and the map,
# so it is also the one that says a program can have all three back.
#
# It does NOT go in /Apps any more. A program with assets gets /Packages/app.<name> and a
# LAUNCHER in /Apps under the bare name that is typed - so what "Lander" names is a script,
# which the shell runs because of what is inside it rather than what it is called. Every
# recorded Lander test still types just "Lander" and knows nothing about any of this, which
# is exactly the claim being made.
"$ROOT/Assembler" -I "$ROOT/Programs/Sounds" -I "$ROOT/Programs/Libraries" \
-I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Lander.asm" -o "$WORK/Lander.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Lander.sbx" >/dev/null
"$TOOL" mkdir "$DISKS/cosmos.img" /Packages >/dev/null
"$TOOL" mkdir "$DISKS/cosmos.img" /Packages/app.Lander >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Lander.sbx" /Packages/app.Lander/Lander.sbx >/dev/null
# Pad.sbx says what the controllers are doing, which is the only way to tell a pad nobody
# noticed from one that is mapped to nothing.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
@@ -188,9 +198,14 @@ printf '#! script\nfor a in 1 2\n for b in x y\n echo $a$b\n end\nend\nset
# Written in the tune language and compiled here, so the music can be changed without
# touching the game. Lander reads it off the disk at startup and starts anyway if it is not
# there, the way it treats /lander.state.
#
# BESIDE THE GAME rather than at the root, which is what the game now asks for: it is told
# where it was loaded from and joins the tune's name to that. /lander.state stays at the root
# on purpose - a saved position is the PERSON's, so it is found where the person is standing,
# and the two of them together are the whole distinction between an asset and a document.
"$ROOT/SoundPatch" --blob "$ROOT/Programs/Sounds/Oboe.json" Oboe "$WORK/oboe.patch" >/dev/null
"$ROOT/TuneC" -I "$WORK" "$ROOT/Programs/Tunes/splash.score" "$WORK/splash.tune" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/splash.tune" splash.tune >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/splash.tune" /Packages/app.Lander/splash.tune >/dev/null
# tune.sbx, which used to be a boot image and is now a program like any other. It brings a
# vector of its own - the timer's, so that it has a beat to play to - which makes it the
@@ -237,6 +252,9 @@ python3 -c "open('twoblocks.txt','w').write('the second disk, at length. ' * 20)
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Copy.sbx" /Apps/Copy.sbx >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Say.sbx" /Apps/Say.sbx >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Where.sbx" /Apps/Where.sbx >/dev/null
# And the launcher for Lunar Porter, under the bare name somebody types. Put here rather than
# beside the game above, because /Apps is not made until now.
"$TOOL" put "$DISKS/cosmos.img" "$ROOT/Programs/CosmOS/Launchers/Lander" /Apps/Lander >/dev/null
# A SCRIPT in the system's place, which is the one thing on this disk that is fetched from
# drive 0 a block at a time WHILE it runs. Its own name carries the drive in front of it,
# and resolving a name like that moves the machine to that drive - so a script started from
+30 -24
View File
@@ -1623,7 +1623,7 @@ for i in range(400):
keys += b'\x80' + b'\x00' * 47
open('$BUILD/lander.keys','wb').write(keys + b'\x00' * 8000)
"
timeout 30 "$EMU" --fast --cycles 900000 --keyboard "$BUILD/lander.keys" \
timeout 30 "$EMU" --fast --cycles 1100000 --keyboard "$BUILD/lander.keys" \
--screen "$BUILD/lander.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/lander.out" 2>&1 || true
read -r MOON SKY SHIP SHIPLEFT SHIPRIGHT <<EOT
@@ -1698,7 +1698,7 @@ print(sum(1 for o in range(0, len(px), 3) if px[o:o + 3] == moon))
# thrusters. The gap between the two captures is what the check is made of, and it is the
# same gap.
python3 -c "open('$BUILD/lander.pad','wb').write(b'\x00' * 40 + b'\x08' * 400)"
for when in 1000000 1800000; do
for when in 1200000 2000000; do
timeout 30 "$EMU" --fast --cycles $when --keyboard "$BUILD/lander.keys" \
--pad "$BUILD/lander.pad" --screen "$BUILD/held$when.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
@@ -1712,7 +1712,7 @@ def top(path):
ship = bytes.fromhex('d8c048')
ys = [i // 320 for i in range(len(px) // 3) if px[i * 3:i * 3 + 3] == ship]
return min(ys) if ys else -1
print(top('$BUILD/held1000000.ppm'), top('$BUILD/held1800000.ppm'))
print(top('$BUILD/held1200000.ppm'), top('$BUILD/held2000000.ppm'))
" 2>/dev/null || echo "-1 -1")
EOT
[ "$EARLY" -gt 0 ] && [ "$LATE" -gt 0 ] && [ "$LATE" -lt "$EARLY" ] \
@@ -1827,7 +1827,7 @@ open('$BUILD/fly.keys','wb').write(b'Lander\n' + SKIP + b'\x00' * 8000)"
python3 -c "open('$BUILD/none.pad','wb').write(b'\x00' * 60000)"
cp "$ROOT/Tests/build/disks/cosmos.img" "$BUILD/orbit.img"
"$ROOT/SplitDisk" put "$BUILD/orbit.img" "$BUILD/orbit.state" /lander.state > /dev/null
for when in 3000000 13000000 25000000 37000000; do
for when in 3200000 13200000 25200000 37200000; do
timeout 60 "$EMU" --fast --cycles $when --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/none.pad" --screen "$BUILD/orbit$when.ppm" \
--disk "$BUILD/orbit.img" --ram-disk 2048 \
@@ -1842,8 +1842,8 @@ def height(path):
ship = [i // width for i in range(len(px) // 3)
if px[i * 3:i * 3 + 3] == bytes.fromhex('d8c048')]
return min(ship) if ship else -1
print(height('$BUILD/orbit3000000.ppm'), height('$BUILD/orbit13000000.ppm'),
height('$BUILD/orbit25000000.ppm'), height('$BUILD/orbit37000000.ppm'))
print(height('$BUILD/orbit3200000.ppm'), height('$BUILD/orbit13200000.ppm'),
height('$BUILD/orbit25200000.ppm'), height('$BUILD/orbit37200000.ppm'))
" 2>/dev/null || echo "-1 -1 -1 -1")
EOT
# A smaller row is higher up. Fifty pixels of change either way, against turning points that
@@ -1976,11 +1976,17 @@ EOT
# is the drift: it grows right from the middle of the screen for a rightward one and left for
# a leftward one, and NOUGHT DRAWS NOTHING, so "stopped" is the state with no bar at all.
#
# The recording holds Right for eighty frames and then nothing. Checked in two places: with a
# drift, where the bar starts at the middle and runs right; and with none, where a sprite of
# no width is a sprite that is not drawn.
python3 -c "open('$BUILD/drift.pad','wb').write(b'\x00' * 20 + b'\x01' * 80 + b'\x00' * 400)"
timeout 30 "$EMU" --fast --cycles 1600000 --keyboard "$BUILD/lander.keys" \
# The recording holds Right for a hundred and twenty frames and then nothing. Checked in two
# places: with a drift, where the bar starts at the middle and runs right; and with none,
# where a sprite of no width is a sprite that is not drawn.
#
# IT WAS EIGHTY, AND THE PAD IS COUNTED FROM THE MACHINE STARTING rather than from the game
# starting. A launcher put twelve frames between those two, so the game saw twelve fewer
# frames of the burn and drew a narrower bar - which is a real reading of a real drift, and
# nothing to do with what this check is about. Longer, so that what is being asked about is
# whether a drift draws a bar rather than how long the pilot held the button.
python3 -c "open('$BUILD/drift.pad','wb').write(b'\x00' * 20 + b'\x01' * 120 + b'\x00' * 400)"
timeout 30 "$EMU" --fast --cycles 1800000 --keyboard "$BUILD/lander.keys" \
--pad "$BUILD/drift.pad" --screen "$BUILD/drift.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
"$BUILD/cosmos.bin" > "$BUILD/drift.out" 2>&1 || true
@@ -2343,13 +2349,13 @@ python3 -c "
pad = bytearray(b'\x08' * 3000)
pad[100:108] = b'\x28' * 8 # Up and B together, so the climb is not interrupted.
open('$BUILD/hoverwide.pad','wb').write(bytes(pad) + b'\x00' * 20000)"
for when in 2000000 3000000 5000000 7000000; do
for when in 2200000 3200000 5200000 7200000; do
timeout 60 "$EMU" --fast --cycles $when --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/hoverwide.pad" --screen "$BUILD/station$when.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
"$BUILD/cosmos.bin" > "$BUILD/station.out" 2>&1 || true
done
timeout 60 "$EMU" --fast --cycles 3000000 --keyboard "$BUILD/fly.keys" \
timeout 60 "$EMU" --fast --cycles 3200000 --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/hovernarrow.pad" --screen "$BUILD/stationnarrow.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
"$BUILD/cosmos.bin" > "$BUILD/stationnarrow.out" 2>&1 || true
@@ -2366,11 +2372,11 @@ def station(path, band):
if px[i * 3:i * 3 + 3] == blue and i // width < band]
return len(at), (min(x for x, y in at) if at else -1)
narrow = station('$BUILD/stationnarrow.ppm', 70)
seen, x3 = station('$BUILD/station3000000.ppm', 200)
seen, x3 = station('$BUILD/station3200000.ppm', 200)
print(narrow[0], seen,
station('$BUILD/station2000000.ppm', 200)[1], x3,
station('$BUILD/station5000000.ppm', 200)[1],
station('$BUILD/station7000000.ppm', 200)[1])
station('$BUILD/station2200000.ppm', 200)[1], x3,
station('$BUILD/station5200000.ppm', 200)[1],
station('$BUILD/station7200000.ppm', 200)[1])
" 2>/dev/null || echo "0 0 -1 -1 -1 -1")
EOT
# Forty pixels is the whole of the art, so none of it is clipped and it is the tile meant.
@@ -2640,7 +2646,7 @@ open('$BUILD/crash.state','wb').write(struct.pack('<hhhhhhBBBB',
"
cp "$ROOT/Tests/build/disks/cosmos.img" "$BUILD/crash.img"
"$ROOT/SplitDisk" put "$BUILD/crash.img" "$BUILD/crash.state" /lander.state > /dev/null
for when in 1200000 1600000 2200000; do
for when in 1400000 1800000 2400000; do
timeout 60 "$EMU" --fast --cycles $when --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/none.pad" --screen "$BUILD/burst$when.ppm" \
--disk "$BUILD/crash.img" --ram-disk 2048 \
@@ -2658,8 +2664,8 @@ def bits(path):
return 0, 0
xs = [x for x, y in at]
return len(at), max(xs) - min(xs)
e = bits('$BUILD/burst1200000.ppm'); l = bits('$BUILD/burst1600000.ppm')
print(e[0], e[1], l[0], l[1], bits('$BUILD/burst2200000.ppm')[0])
e = bits('$BUILD/burst1400000.ppm'); l = bits('$BUILD/burst1800000.ppm')
print(e[0], e[1], l[0], l[1], bits('$BUILD/burst2400000.ppm')[0])
" 2>/dev/null || echo "0 0 0 0 -1")
EOT
# Forty pixels in one eight wide block is the lander; anything else in its colour is what is
@@ -2671,7 +2677,7 @@ EOT
&& result ok "and they carry on outwards" "$EARLYWIDE pixels apart, then $LATEWIDE" \
|| result no "and they carry on outwards" "$EARLYWIDE pixels apart, then $LATEWIDE"
# And they are gone by the time the verdict is up, rather than hanging over the words.
[ "$AFTERBITS" = "0" ] && grep -q "Crashed" "$BUILD/burst2200000.out" \
[ "$AFTERBITS" = "0" ] && grep -q "Crashed" "$BUILD/burst2400000.out" \
&& result ok "and are gone before the verdict" "nothing left over the words" \
|| result no "and are gone before the verdict" "$AFTERBITS pixels still there"
@@ -2933,7 +2939,7 @@ done
# And a plain fall onto the pad it starts over, which lands rather than crashes.
# Eight frames later than they used to be: the landing latch plays its two notes before the
# dust goes up, and runPend waits for the second of them.
for when in 2900000 3050000 3300000; do
for when in 3100000 3250000 3500000; do
timeout 60 "$EMU" --fast --cycles $when --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/none.pad" --screen "$BUILD/dust$when.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
@@ -2953,8 +2959,8 @@ def look(path):
puff = len([1 for x, y in where('f0f0f0') if y > 20])
ship = where('d8c048')
return puff, (min(y for x, y in ship) if ship else -1)
print(look('$BUILD/dust2900000.ppm')[0], look('$BUILD/dust3050000.ppm')[0],
look('$BUILD/dust3300000.ppm')[0],
print(look('$BUILD/dust3100000.ppm')[0], look('$BUILD/dust3250000.ppm')[0],
look('$BUILD/dust3500000.ppm')[0],
look('$BUILD/gas9900000.ppm')[0], look('$BUILD/gas10150000.ppm')[0],
look('$BUILD/gas10800000.ppm')[0],
look('$BUILD/gas10150000.ppm')[1], look('$BUILD/gas10800000.ppm')[1])