A dock that slides into line, and settles squarely on the port

Two things, and the second is the one that was actually wrong.

The lander is slid into place at half a pixel a frame rather than put
there. A dock is allowed eight pixels out in either direction, so
snapping moved it a whole tile in a single frame - a jump, at the very
moment the player was being told they had been careful. The worst gap
closes in about half a second, which reads as the two of them settling
together.

And it settles SQUARELY now. It used to come to rest four pixels out
however carefully it was flown, because the lander is drawn from half a
screen less half a tile - which is what centres an eight pixel lander on
the middle - while the station was drawn from half a screen exactly, so
its left edge sat where the lander's centre was. Both come off the same
origin now, and a gap of nothing puts one exactly above the other.

stationGap is factored out along the way. Three callers wanted it: the
one that draws the station, the one that decides whether it can be docked
with, and now the one that slides the lander in under it. The sideways
ease goes through that wrapped gap rather than the raw positions, because
a dock made either side of the moon's seam has a raw difference of most
of a moon.

---- And the fixtures moved to frame 400 ----

A dock waits to be told its message has been read, and reading a state
file costs a disk read, so a placed run starts a good deal later than a
plain one. The acknowledgement was at frame 100 and stopped working the
day the state file arrived: the program had not reached the dock yet and
the press went by unheard, which shows up as every sprite missing and
reads like a drawing bug. Everything after it is sampled well clear of
both ends.

Two checks, each seen to fail on its own break - the alignment settles at
4,-8 without the shared origin, and the slide reads 0,-8 the whole way
without the easing. Which of the two axes each one actually watches is
written down beside them, because it is not the one you would guess.
This commit is contained in:
Anachronaut
2026-09-04 11:23:54 -04:00
parent 1928275f87
commit 7e82b64d47
13 changed files with 230 additions and 77 deletions
+159 -57
View File
@@ -1282,7 +1282,17 @@ moveStation:
; Off the edge needs no test. A sprite's X is signed and sixteen bits, so a station three
; hundred pixels to the left is asked for at minus a hundred and forty and the device declines
; to draw it, which is a comparison this does not have to make.
showStation:
; ---- How far round the moon the station is from the lander, and which way ----
;
; Wrapped to fourteen bits like everything else here, which measures the long way round
; whenever the station is behind - so anything past the half way point is turned into a
; distance BACKWARDS before anybody uses it. Three callers want this now: the one that draws
; the station, the one that decides whether it can be docked with, and the one that slides the
; lander in under it afterwards.
;
; Half a moon is 8,192 sixteenths, which is 512 pixels - and that fits in the twelve signed
; bits toPixelsSigned hands back, so the drawing side can take this straight.
stationGap:
SETD.0 StationLow
LDA.0
SETD.1 AcrossLow
@@ -1301,24 +1311,33 @@ showStation:
AND
SETD.1 GapHigh
STQ.1
SETD.0 GapLow
CALL toPixels ; Nought to 1023 pixels, which is the whole moon.
SETD.0 PixelHigh
LDA.0
INIB 0x02
LDA.1
INIB 0x20
AND
BRQ stationAhead ; Under 512, so it is ahead and the offset stands.
BRQ stationGapDone ; Under half a moon, so it is ahead and the gap stands.
SETD.0 GapHigh
LDA.0
INIB 0d4
INIB 0x40
CCF
SUB
STQ.0 ; Less a whole moon, which makes it a distance backwards.
stationAhead:
stationGapDone:
RET
showStation:
CALL stationGap
SETD.0 GapLow
CALL toPixelsSigned
; ---- From the lander's own column, not from the middle ----
;
; ShipX is half a screen LESS HALF A TILE, because that is what centres an eight pixel lander
; on the middle. The station was drawn from the middle itself, so its left edge sat where the
; lander's centre was and a perfectly aligned dock still looked four pixels out. Same origin
; for both, and a gap of nothing puts one exactly above the other.
SETD.0 PixelLow
LDA.0
SETD.1 HalfScreen
SETD.1 ShipX
LDB.1
CCF
ADD
@@ -1326,7 +1345,7 @@ stationAhead:
STQ.1
SETD.0 PixelHigh
LDA.0
SETD.1 HalfScreen
SETD.1 ShipX
INCD.1
LDB.1
ADD
@@ -1802,41 +1821,9 @@ dock:
LDA.0
BNA dockRiding
; ---- Close enough, going round ----
;
; Wrapped to fourteen bits like everything else on this moon, which measures the long way
; round when the station is behind, so past the half way point is turned into a distance
; backwards before its size is taken.
SETD.0 StationLow
LDA.0
SETD.1 AcrossLow
LDB.1
CCF
SUB
SETD.1 DockGapLow
STQ.1
SETD.0 StationHigh
LDA.0
SETD.1 AcrossHigh
LDB.1
SUB
MVQA
INIB 0x3F
AND
SETD.1 DockGapHigh
STQ.1
LDA.1
INIB 0x20
AND
BRQ dockGapNear
SETD.0 DockGapHigh
LDA.0
INIB 0x40
CCF
SUB
STQ.0
dockGapNear:
SETD.0 DockGapLow
; Close enough, going round, which is the same gap the station is drawn from.
CALL stationGap
SETD.0 GapLow
CALL magnitude
SETD.0 DriftHigh
LDA.0
@@ -1951,27 +1938,35 @@ dockRiding:
AND
BNQ dockLetGo
SETD.0 StationLow
LDA.0
SETD.1 AcrossLow
STA.1
SETD.0 StationHigh
LDA.0
SETD.1 AcrossHigh
STA.1
; ---- Slid into place, not snapped to it ----
;
; A dock is allowed at up to eight pixels out in either direction, so putting the lander
; exactly under the station the instant it takes hold moved it a whole tile in one frame -
; a jump, right at the moment the player was being told they had been careful. Half a pixel
; a frame closes the worst of it in about half a second, which reads as the two of them
; settling together.
;
; The sideways one goes through the WRAPPED gap rather than the raw positions, because a
; dock made either side of the moon's seam has a raw difference of most of a moon.
CALL stationGap
CALL easeAcross
SETD.0 StationDownLow
LDA.0
INIB 0d128
CCF
ADD
SETD.1 DownLow
SETD.1 DockTargetLow
STQ.1
SETD.0 StationDownHigh
LDA.0
RSTB
ADD ; One tile below it, carry included.
SETD.1 DownHigh
SETD.1 DockTargetHigh
STQ.1
SETD.0 DownLow
SETD.1 DockTargetLow
CALL easeToward
INIA 0d64
SETD.0 SpeedAcross
@@ -1991,6 +1986,100 @@ dockLetGo:
STA.0
RET
; ---- Half a pixel of the way there, or the whole of it if that is nearer ----
;
; DP0 names the word to move, DP1 the place it should end up. The gap is taken as a signed
; sixteen bit number, so "close enough to finish" is two cases and not one: a small gap
; forwards has a high byte of nought, and a small gap backwards has one of 0xFF.
easeToward:
LDA.1
LDB.0
CCF
SUB
SETD.2 EaseLow
STQ.2
INCD.0
INCD.1
LDA.1
LDB.0
SUB ; And the borrow, which is the other half of the gap.
SETD.2 EaseHigh
STQ.2
DECD.0
DECD.1 ; Both pointers back at the low bytes they came in on.
LDA.2 ; DP2 still names the high half of the gap.
BNA easeBack ; Not nought, so the gap runs backwards or is a long way off.
SETD.2 EaseLow
LDA.2
INIB 0d8
CCF
SUB
BRC easeArrive ; Borrowed: under a step, so put it exactly there.
SETD.1 EaseForward
CALL addWord
RET
easeBack:
SETD.2 EaseLow
LDA.2
INIB 0d248
CCF
SUB
BNC easeArrive ; No borrow: within a step of nought, going the other way.
SETD.1 EaseBackward
CALL addWord
RET
easeArrive:
LDA.1
STA.0
INCD.0
INCD.1
LDA.1
STA.0
RET
; ---- The same, for a position that has to go round a moon to get there ----
;
; The gap is already worked out and wrapped, in GapLow. Adding to a position and then wrapping
; it is the whole difference from the routine above: on a ring the target is "whatever closes
; that gap", not a place, because the place can be on the other side of the seam.
easeAcross:
SETD.0 GapHigh
LDA.0
BNA easeAcrossBack ; Not nought, so the gap runs backwards or is a long way off.
SETD.0 GapLow
LDA.0
INIB 0d8
CCF
SUB
BRC easeAcrossWhole ; Borrowed: under a step, so close the whole of it at once.
SETD.0 AcrossLow
SETD.1 EaseForward
CALL addWord
BRI easeAcrossWrap
easeAcrossBack:
SETD.0 GapLow
LDA.0
INIB 0d248
CCF
SUB
BNC easeAcrossWhole
SETD.0 AcrossLow
SETD.1 EaseBackward
CALL addWord
BRI easeAcrossWrap
easeAcrossWhole:
SETD.0 AcrossLow
SETD.1 GapLow
CALL addWord
easeAcrossWrap:
SETD.0 AcrossHigh
LDA.0
INIB 0x3F
AND
STQ.0 ; Round the moon, the way every other step along it is.
RET
; ---- A key, or a button ----
;
; Somebody flying on a controller should not have to reach for the keyboard to say "yes, I
@@ -3104,6 +3193,19 @@ Landed:
; Holding onto the station, which is the same kind of rest as sitting on the ground.
Docked:
0x00
; Where the lander is sliding to, and how far it has to go to get there.
DockTargetLow:
0x00
DockTargetHigh:
0x00
EaseLow:
0x00
EaseHigh:
0x00
EaseForward:
0x08 0x00 ; Half a pixel a frame, which settles the worst gap in about half
EaseBackward:
0xF8 0xFF ; a second - slow enough to see and fast enough not to wait for.
DockGapLow:
0x00
DockGapHigh:
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -25,7 +25,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -15,7 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -32,7 +32,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -108,7 +108,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -27,7 +27,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -15,7 +15,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -13,7 +13,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+1 -1
View File
@@ -22,7 +22,7 @@ Mode.sbx 48
Flip.sbx 173
Sprite.sbx 442
Depth.sbx 672
Lander.sbx 6692
Lander.sbx 6814
Pad.sbx 264
Crash.sbx 632
vars.script 50
+60 -9
View File
@@ -2379,8 +2379,27 @@ for which in place dock wreck; do
cp "$ROOT/Tests/build/disks/cosmos.img" "$BUILD/$which.img"
"$ROOT/SplitDisk" put "$BUILD/$which.img" "$BUILD/$which.state" /lander.state > /dev/null
done
python3 -c "open('$BUILD/ackonly.pad','wb').write(b'\x00' * 200 + b'\x10' * 8 + b'\x00' * 60000)"
for run in place:3000000 dock:6000000 dock2:9000000 wreck:3000000; do
# ---- The acknowledgement comes at frame 400, and that is not a round number by accident ----
#
# A dock, like a landing, waits to be told its message has been read. Reading a state file
# costs a disk read, so a placed run starts a good deal later than a plain one - the press was
# at frame 100 and stopped working the day the state file arrived, because by then the program
# had not reached the dock yet and the press went by unheard. Everything after it is sampled
# well clear of both ends.
python3 -c "open('$BUILD/ackonly.pad','wb').write(b'\x00' * 400 + b'\x10' * 8 + b'\x00' * 60000)"
# A dock that is legally within tolerance but well out of line - six pixels back and six up -
# so there is a visible distance to slide. Sampled twice: once while it is still closing, and
# once after. The slide runs for the thirty or so frames after the acknowledgement at frame
# 400, and the middle sample sits at about frame 419; if it ever fails, check that before
# believing the code is broken.
python3 -c "
import struct
open('$BUILD/slide.state','wb').write(struct.pack('<hhhhhhBBBB',
0x34A0 - 96, -1536 - 96, 64, 0, 0x34A0, -1536, 100, 1, 0, 0))
"
cp "$ROOT/Tests/build/disks/cosmos.img" "$BUILD/slide.img"
"$ROOT/SplitDisk" put "$BUILD/slide.img" "$BUILD/slide.state" /lander.state > /dev/null
for run in place:5000000 dock:7300000 dock2:9000000 wreck:5000000 slide:7000000 slide2:8000000; do
which="${run%%:*}"; when="${run##*:}"
timeout 60 "$EMU" --fast --cycles "$when" --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/ackonly.pad" --screen "$BUILD/$which.ppm" \
@@ -2388,11 +2407,11 @@ for run in place:3000000 dock:6000000 dock2:9000000 wreck:3000000; do
"$BUILD/cosmos.bin" > "$BUILD/$which.out" 2>&1 || true
done
# And the same moment with no state file at all, which is the contrast that says the file did it.
timeout 60 "$EMU" --fast --cycles 3000000 --keyboard "$BUILD/fly.keys" \
timeout 60 "$EMU" --fast --cycles 5000000 --keyboard "$BUILD/fly.keys" \
--pad "$BUILD/ackonly.pad" --screen "$BUILD/nostate.ppm" \
--disk "$ROOT/Tests/build/disks/cosmos.img" --ram-disk 2048 \
"$BUILD/cosmos.bin" > "$BUILD/nostate.out" 2>&1 || true
read -r PLACEDY PLACEDFUEL PLAINFUEL DOCKFUEL DOCK2FUEL RIDESHIP RIDESTATION <<EOT
read -r PLACEDY PLACEDFUEL PLAINFUEL DOCKFUEL DOCK2FUEL SLIDING SETTLED <<EOT
$(python3 -c "
def look(path):
d = open(path, 'rb').read()
@@ -2409,8 +2428,23 @@ def look(path):
return (min(y for x, y in ship) if ship else -1, fuel,
'%d,%d' % (min(x for x, y in ship), min(y for x, y in ship)) if ship else 'none',
'%d,%d' % (min(x for x, y in station), min(y for x, y in station)) if station else 'none')
def offset(path):
# Where the station sits relative to the lander, which is what alignment means here.
d = open(path, 'rb').read()
width = int(d[:40].split()[1])
px = d[d.index(b'255\n') + 4:]
def where(colour):
c = bytes.fromhex(colour)
return [(i % width, i // width) for i in range(len(px) // 3)
if px[i * 3:i * 3 + 3] == c]
ship = where('d8c048'); station = [q for q in where('5880e0') if q[1] < 250]
if not ship or not station:
return 'none'
return '%d,%d' % (min(x for x, y in station) - min(x for x, y in ship),
min(y for x, y in station) - min(y for x, y in ship))
p = look('$BUILD/place.ppm'); d = look('$BUILD/dock.ppm'); d2 = look('$BUILD/dock2.ppm')
print(p[0], p[1], look('$BUILD/nostate.ppm')[1], d[1], d2[1], d2[2], d2[3])
print(p[0], p[1], look('$BUILD/nostate.ppm')[1], d[1], d2[1],
offset('$BUILD/slide.ppm'), offset('$BUILD/slide2.ppm'))
" 2>/dev/null || echo "-1 -1 -1 -1 -1 none none")
EOT
# Placed at the world's origin with a hundred units: row 192 is the origin in the wide view,
@@ -2424,10 +2458,27 @@ EOT
[ "$DOCKFUEL" = "22" ] && [ "$DOCK2FUEL" = "22" ] \
&& result ok "a matched approach docks and is paid" "a hundred became a hundred and eighty, once" \
|| result no "a matched approach docks and is paid" "$DOCKFUEL cells, then $DOCK2FUEL"
# Held to it, one tile under, rather than merely paid by it.
[ "$RIDESHIP" = "316,104" ] && [ "$RIDESTATION" = "320,96" ] \
&& result ok "and rides it, a tile under" "lander $RIDESHIP, station $RIDESTATION" \
|| result no "and rides it, a tile under" "lander $RIDESHIP, station $RIDESTATION"
# ---- And it settles ALIGNED, which took finding ----
#
# Nought across and eight up is the station squarely over the lander, a tile apart. It used to
# come to rest four pixels out however carefully it was flown, because the lander is drawn from
# half a screen LESS HALF A TILE - which is what centres an eight pixel lander on the middle -
# and the station was drawn from half a screen exactly. Same origin for both now.
[ "$SETTLED" = "0,-8" ] \
&& result ok "and rides it, squarely under the port" "the station directly above, a tile up" \
|| result no "and rides it, squarely under the port" "settled at $SETTLED, wanted 0,-8"
# Slid, not snapped. A dock is allowed eight pixels out either way, so putting the lander
# exactly in place the instant it took hold moved it a whole tile in one frame - a jump, at
# the very moment the player was being told they had been careful.
#
# THIS WATCHES THE VERTICAL ONE. Sideways has less ground to cover - eight pixels against
# sixteen, since the lander also has to come down a tile - so it has finished by the time this
# sample is taken, and deleting its easing does not show up here. Where it does is the check
# above: sideways is the axis alignment is measured on, so a snap there still has to land
# squarely. The gradualness of it alone is not separately guarded.
[ "$SLIDING" != "$SETTLED" ] && [ "$SLIDING" != "none" ] \
&& result ok "and slides in rather than snapping" "$SLIDING while closing, $SETTLED after" \
|| result no "and slides in rather than snapping" "$SLIDING while closing, $SETTLED after"
grep -q "Wrecked against the station" "$BUILD/wreck.out" \
&& result ok "and the same approach unmatched is a wreck" "64 sixteenths, eight times the limit" \
|| result no "and the same approach unmatched is a wreck" "no wreck in the transcript"