35c6708e4611103a8b092bb298a3a8f1d8eda05a
107
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
35c6708e46 |
The shell takes spaces off both ends of a line, not just the front
Tab completion leaves a space after the word it finished, which is right when another word is coming. When nothing else was coming, that space stayed on the end of the line and a command taking a file name looked for one whose name ended in a space: load greet.sbx loaded, starting at 5000 load greet.sbx no such file (the same line, finished with Tab) Which took most of the good out of completion, since finishing a name and pressing Return is the whole of what it is for. The existing tab tests all typed more characters after completing, so none of them ever submitted a completed line. lineTrim already took the leading spaces off for indented blocks, so the trailing ones come off in the same place, on the whole line, rather than at each of the dozen commands that take a name. Walking back needs no guard against running off the front: by then the first character cannot be a space, and a line that was nothing but spaces has already become an empty one. CONSEQUENCE WORTH SEEING: echo no longer prints a trailing space it was given, which is why cosmosTabPath's recording moved. That is the conventional behaviour and it means what echo is handed is what somebody would have typed, but it is a real change and not only a bug fix. cosmosTrim covers a line completed and run straight away, the same trailing space typed by hand, spaces at both ends at once, and a line of nothing but spaces, which still has to do nothing rather than fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d361ea1e46 |
An LFO belongs to its channel, not to the whole device
The two LFOs lived in the Synth, so four channels shared them and whichever patch loaded last owned them for every voice at once. A sound with its LFO switched off silenced the trill under a sound that was still playing - which is what made Lunar Porter's low fuel warning intermittent: the first landing, docking or crash of a run took its trill away, and it was right again next time the machine started. The engine fix went upstream to soundThing and has come back. synth.c and synth.h are re-vendored at 71e3cb2, character for character bar the ASCII transliteration, and now carry two changes: the LFOs moved into the Voice, and synthSyncVoices carries a free LFO's cycle down alongside its rate. That second hunk does nothing here - it only matters to a caller that syncs voices, and this device never does, because syncing would flatten four channels into one instrument. It is taken so the vendored file stays identical in both trees, and it is commented as such. Upstream also found a bug in the original patch, in patchLoad, which is soundThing's own file and does not travel. Downstream the LFO parameter groups 0x60 and 0x70 now read the selected channel like every parameter beside them, so an LFO written to one channel is inaudible on the other three. Everything else about the device is unchanged. Lunar Porter keeps loading each patch immediately before its note, but for the smaller reason that now applies: the bang and the latch share channel three, and a channel used by two sounds has to be told which of them it is about to be. The comment that said otherwise, and the manual's warning about sharing, are rewritten as history rather than as a caveat. Tests/sound.sh's shared-LFO check is inverted to assert the fixed behaviour, with a third leg added: after proving another channel's patch leaves this one alone, it switches this channel's OWN LFO off and requires the pitch to move. Without that, both checks would pass on a device where writing an LFO did nothing at all. Routing either group back to voice 0 is caught. Cost, measured: four channels sounding continuously for 400 seconds of audio takes 5.0 s of wall clock against 4.59 s before, about 9% of total emulator time. Half of that is wasted on voices that cannot sound, since VOICE_COUNT is 8 and there are four channels; recovering it would mean diverging the vendored file, which is not worth it at this price. |
||
|
|
3ca5f193e6 |
The warning's trill, and why it only sometimes came out
An LFO belongs to the DEVICE and not to a channel. There are two of them against four voices, and a patch carries LFO settings the way it carries everything else - so whichever patch was loaded last owns both of them, for every voice at once. The warning's trill is a saw LFO on the pitch. The patches on channel three, the bang and the latch, carry an LFO that is switched off, and they load at the moment they are used. So the first landing, docking or crash of a run took the trill away and left a plain tone, and it was right again next time the machine started. Correct until something unrelated plays is the worst shape a fault can have. The same thing had already happened silently at startup: the instruments were set up in order, so the warning's LFO settings, written last, sat on top of the rumble's and the rumble never had its own at all. So nothing is set up once any more. Each sound loads its patch immediately before its note - forty-odd writes at a moment already making a sound - and is then whatever its patch says, whatever played before it. Measured after a landing: 1056, 660, 516 hertz, then up to 1698 and down again. Two sweeps of the saw, which is the trill. What it does not fix, because it cannot: two sounds overlapping still share the LFOs, so a warning going off mid-burn re-tunes the rumble for as long as it lasts. With two between four that is the device. Three checks at the device level, where the trap can be stated exactly: a routed LFO bends a pitch (184 Hz against the 262 the note asked for), another channel's patch takes it away (272, the note itself), and saying it again gets it back (184). Written up in the Programming Manual beside the LFO mode, since the next program to want two sounds will meet it too. |
||
|
|
5e85356245 |
Every thruster that catches pops, not only the first
A pilot already burning upwards who then adds a sideways thruster has lit an engine, and that is what an engine lighting sounds like. So what is watched is now the SET of thrusters rather than whether any of them is lit: the bits on now that were not on before, which is an exclusive or and an and and no comparison at all. The rumble still asks the old question, because it is the right question for it - struck when the first lights and not again until every one has gone out. Restriking it when a second joins would start its attack over, which is a stutter rather than an engine, so it keeps a flag of its own. Pitches are as tuned by ear: 36 for the pop, 60 for the rumble. Checked by two flights of the same length, one thruster held throughout against one that gains a second in the middle. The rumble is identical in both, so the whole of the difference is the extra pop: peak 11,452 against 15,540, and the recordings diverge at 3.19 seconds, which is the frame the second thruster lights. |
||
|
|
388faafd04 |
A thruster you can hear, and a latch on arriving and leaving
Three patches, and the last of the sounds that were asked for. The thruster bangs when it lights and rumbles while it burns. The rumble is this game's first HELD note: its gate goes down when a thruster lights and does not come up until every one is out. Only the edges matter - a rumble restruck every frame would never get past its own attack, and a bang struck every frame is a buzz - and the condition is the flames': a held button with a dry tank is a pilot doing nothing. Arriving latches two notes quickly. Middle C then the C above for taking hold of the station, the same pair reversed for letting go, and two octaves lower for the ground - the same shape in a different register, because setting down and taking hold are the same kind of event. The second note is PENDING rather than played: at an undocking the pilot is mid-burn, and stopping the world for an eighth of a second to fit a note in would be felt as the controls sticking. Where the game is stopping anyway, runPend simply pumps it out. Four channels for five sounds. The bang and the latch share one, because a lander arriving either arrives or does not, and a crash ends the run. That leaves the thruster its two, which it needs: a held note struck on the same channel as the ignition bang would cut the bang off at the moment it was meant to be heard. ---- A held note outlives the loop that was holding it ---- Landing while the thruster was still down ended the flying and then waited to be told the message had been read, so the frame that would have noticed the button coming up never ran. The engine roared under the verdict and went on roaring until the machine stopped. Anything that stops to wait hushes it now, and forgets last frame with it, so a thruster still held when the waiting ends counts as lighting again. ---- Three checks that had to be rebuilt around the new noise ---- A landing is not silent any more, so the crash is measured against the second BEFORE it rather than against a quiet landing. The dust samples moved eight frames later, because the latch plays first. And the warning check lost its measure twice: counting bursts could not tell a beep from a nag, and measuring total length stopped working the day the thruster got a rumble. It burns, stops, and listens AFTER - warned once there is nothing left, nagging the last beep is still fading. Nought against 5,679. |
||
|
|
6fe898b5fc |
A quarter-tank warning that says it once, and a gauge that keeps saying it
Two halves of the same number. The warning is the moment it happened and the colour is how things stand: it fires on the way down through a quarter of a tank and then holds its peace, and the gauge stays red until a base fills the lander up, which also allows the warning again. Once, because a lander is at its most careful in the last few seconds before it touches, and something repeating in its ear through that is not a warning, it is a distraction. Checked where the fuel actually moves rather than once a frame - the tank only changes in takeFuel and payFuel, so there is nowhere else it can cross a threshold. Channel two, with the crash on three. Separate channels rather than one reused, because a crash while the warning is still sounding should not cut it off, and on a machine with four voices there is no reason to be clever. Half and a tenth are the obvious next thresholds and are deliberately not here: one is enough to find out whether being told at all is welcome. ---- And the crash sound is the one that was designed for it ---- Crash.json, which carries voice_gate itself, so the program's own trigger write is gone - it would have masked a deliberate choice rather than backing one up. The note moved from 24 to 60 by ear, and the comment saying it was low went with it. ---- A check that could not tell a beep from a nag ---- Counting bursts of sound cannot: without the latch the warning fires every tick the tank drops, far faster than the sound decays, so the beeps run into each other and a burst counter sees one long burst either way. It measures the LENGTH now - 26,944 samples against 490,348 - which is the difference between six tenths of a second and ten seconds of it. |
||
|
|
b2ff8d64e5 |
Fold soundThing's changes back down, and expose the two new switches
The three changes that went up came back as part of soundThing, along with two more that they made possible. The engine here is now b73e5c0 character for character, except that em-dashes and arrows in comments are written as ASCII because this tree is ASCII only - a local rule, not an improvement, and not sent up. So synth.h's "what was changed" list is gone. There is nothing to list: what has to be kept current is only that if either copy changes, the other one has to be told. ---- What came back ---- A VOICE CAN END ITSELF. Naming the level's source said what shapes a voice; nothing said what ends one, so the only thing that could ever finish one was a key coming up. A game is nearly all one-shots and not one of them wants its length decided by how long a note was held. Exposed as parameter 0x51: 0 gated, 1 triggered. AND A ONE-SHOT IS THE SAME ONE-SHOT TWICE. A triggered voice re-arms its oscillators, and an LFO can be told to start over with each voice - parameter 3 of either LFO. Both halves are needed and the check proves it: with the LFO left free, two triggered hits still differ. Their note warned that whatever applies a patch to a channel has to set these or they hold synthInit's defaults. Checked: Voyager never calls synthSyncVoices, so their 0001 is a no-op here as they predicted, and nothing reaches into an LFO's phase, so the struct split is safe. ---- What it is for ---- Lander's crash is a triggered voice now, so boomOff is gone. Nothing has to remember to end a bang. SoundPatch learnt voice_levelSource, voice_gate and lfo<N>_mode, which the new soundThing writes - without that it would have refused every patch saved from it, since an unknown field stops the tool on purpose. A patch from before those fields still converts, and says in its own comments that it predates the level routing. Three checks, each seen to fail on its own break: a gated voice still sounding with nothing holding it, a triggered one down to nothing with no gate ever dropped, and two hits identical sample for sample. One test bug worth keeping: the first version of the repeatability check struck the second note while the first was still ringing, so what it found and compared as "the second hit" was a point in the middle of the first one's tail. It now looks for sound after SILENCE rather than sound after an offset. |
||
|
|
9ae59bfccb |
A bang for the crash, which is this game's first sound
Noise through a low pass that the modulation envelope shuts as the level falls, so the bright part is only at the front of it: a boom rather than a hiss. Noise because every other waveform here has a pitch, and a pitched bang is a note. The instrument is built at startup the way the tiles are, and a crash only says "this channel, this note". That is what the selector and value registers are for - a patch is twenty odd writes and a note is two - and it is the shape every sound after this one should take. CHANNEL THREE. There are four, and effects count down from the top so that music, if it ever arrives, can take nought and count up and the two never have to negotiate. This is also the first program to drive the sound device while also doing something else; the only other customer is the patch editor, whose whole job is the device. ---- A byte of envelope is not seconds ---- It is squared and scaled to four of them, so the decay first written here was 200 - which is two and a half seconds. Over the eight tenths of a second the pieces are in the air that is not a bang fading, it is the FRONT THIRD of one, and it both sounded and measured as a flat wash of noise. Ninety is about half a second and it fades to silence with time to spare. Two checks, each seen to fail on its own break. The first is against SILENCE - a landing in the same conditions makes no sound at all, so it is measuring the crash and not the machine humming - and the second is that it is louder in its first half than its second, which is the difference the decay was getting wrong. |
||
|
|
0264b19a3d |
Dust on landing, gas on letting go, and a lander that stays let go
One particle system, three uses now: a lander coming apart, dust kicked up by a landing, and gas out of a docking port on release. Where they start, how fast they go, what colour they are and how long they last are arguments; everything else is shared. Dust goes sideways and UP off the lander's feet, because that is where kicked dust goes and there is ground in the way of the rest of it. Gas goes evenly in every direction, because nothing is in the way of a docking port. Neither happens on docking - a dock is a catch and not a touchdown, and there is nothing under it to kick. ---- Ticked from the frame loop, not run in place ---- The explosion can afford to stop the world; there is nothing left to fly. The undocking puff cannot, because it goes off on the frame a thruster is pressed, and freezing a quarter of a second exactly then is felt as the controls sticking. So a burst advances one frame at a time from the main loop, and the two that can afford to wait just pump that same tick until the air is clear. ---- And letting go did not let go ---- Which the puff is what found. A docked lander sits EXACTLY one tile under the station, so releasing upwards moved it towards the station and it docked again on the very next frame: took the fuel again, said so again, and waited to be told the message had been read - which reads as the controls locking up the instant they are used. It has to get clear now before it can take hold again, and the two distances have to differ: docking wants one tile, re-arming wants two. A single distance re-armed on the frame it let go, because a tile is exactly where it was sitting. Three checks, each seen to fail on its own break. The last of them measures HOW FAR the lander has got and not merely that it moved: a sixteen frame pause on release still leaves it climbing, four rows short of a free run, so "it moved" would pass for a stall that has been slept through. |
||
|
|
115efa1fa1 |
A crash takes the lander apart, instead of just saying so
The verdict used to be the whole of it: a line of text and a lander still sitting there in one piece, so somebody watching a recording had to read the words to know what had happened. That is the same problem the flames were for. The lander goes, and both flames with it, and six pieces of it leave in a rough hexagon at its own colour for about a second. Then they go too, rather than hanging over the words. The world is not running while it plays: it is a loop of its own, so nothing else in the program has to know how to be half destroyed. Yellow, which is the lander's own colour, because it IS the lander - and it is only free to use because the lander itself is hidden by then. The check that counts exactly forty pixels of yellow looks at a flying frame. ---- Two mistakes worth keeping ---- The block went in between touchdownCrash and touchdownStop, so a crash fell into the explosion and returned from there - no verdict, no end of run, and the lander sitting there being crashed into the ground again every frame. The linter caught it as a subroutine nothing called walking into, which is exactly what it was. And copyWord goes DP0 to DP1, so setting the pieces off from the lander's position had the pointers the wrong way round: it copied the empty pieces OVER ShipX. Since that is in the view block, it took the lander's own column with it, and the one piece that could be seen drifted out of the top left corner of the screen. Three checks, each seen to fail on its own break. They measure the SPREAD and not the count: two of the six leave the top of the screen on the way, so the count drops from twenty four to sixteen, which is correct and would make an exact count a check that breaks the day a lander crashes somewhere else. |
||
|
|
c408fc6cf6 |
Thruster flames, so a watcher can see what the pilot is doing
Every reading on this screen is a number drawn as a bar - how fast sideways, how fast down, how much sky, how much tank - and all of it says what is happening TO the lander. None of it says what the pilot is doing about it, so somebody watching over a shoulder has to read gauges to work out that a thruster is even lit. A plume hangs off whichever side the engine is pushing from: under the lander to lift, over it to retro, and on the far side from the way it is being pushed sideways, since that is the side the gas leaves. One tile does up and down, because a vertical flip turns one into the other, and a second does the sides, because a flip cannot rotate a tile a quarter turn. Twenty four pixels each, on purpose, so a count of them means something. HELD, NOT FIRED. The engine fires one frame in ten, because that is the tick gravity is applied on, and a flame that honest would be one frame of light six times a second - a fault lamp, not a rocket. What is drawn is the button being down, which is the truthful answer to "is the pilot burning": the tick is how the sum gets done, not what is happening. An empty tank draws nothing, and nor does the retro thruster on the ground, because in both cases the button really is doing nothing. The second of those was a lie the first version told. Red, and that is by elimination again: white is the ceiling warning, cyan the landing pads, magenta the instruments, blue the station, yellow the lander itself - and the lander is counted as exactly forty pixels of yellow, so a yellow flame would have broken it. Three checks, each seen to fail on its own break. Two things the fixtures taught: a lander placed at the world's origin sits BEHIND the two row window, which reads exactly like a flame that is not drawn; and red has to be looked for in a box round the lander rather than a column, because the speed bars go red and one of the four pads is red too. |
||
|
|
418631a221 |
The orbit check is back, and osFileRead says it reads in blocks
Placing a state retired the reason the orbit check was deleted. Reaching a given orbit through the controls takes a sustained burn while holding height, and the phase of that burn against the gravity tick - one frame in ten - decides whether the thruster is seen at all, so two pad files a frame apart fly differently. The old check passed against one disk and failed against another, which is a check measuring the boot time rather than the physics. Placed at eighty sideways it climbs to row 51, falls to row 190, and climbs again to row 20 - and the turning points are BROAD, tens of pixels across, so the samples have nothing like the margin problem the old one had. Three claims: it climbs, it turns over on its own, and it comes round again no lower than the first time. Both halves of the mechanic are separately caught. Without the outward push it sinks and lands and never climbs; without the exchange it climbs away and never comes back, which is the one way trip the whole thing exists to prevent. ---- And osFileRead writes whole blocks, which nothing said ---- A disk is read a block at a time, so a sixteen byte file still puts 256 bytes where it is told to. Reserving exactly the file's length writes over whatever follows - a quiet corruption rather than a refusal, and it looks like a bug somewhere else entirely. It cost an afternoon here: the state buffer sat in front of the view tables, so the program read its state, wiped the numbers every gauge draws from, and left immediately. Said now in services.asm beside the vector and in the CosmOS manual, along with the pattern that works: reserve the length rounded up to the next 256, read into that, and copy the parts wanted where they are wanted. The orbit fixture also needs its own keyboard file. The shared one holds a key down every forty eight bytes for the held-thruster check, which would fly this orbit as well as measure it. |
||
|
|
7e82b64d47 |
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. |
||
|
|
1928275f87 |
A state can be placed, which four things were queued behind
Lander reads sixteen bytes from /lander.state if the disk has one and starts from those: position, velocity, where the station is, fuel, and which screen to be in. A disk without the file is the game as it always was, which is every other flight in the suite. It exists because some states cannot be flown to. A successful dock needs the lander alongside the station and matched, and NINETY SIX pad files failed to get there - not for want of trying, but because climbing spends sideways speed, so a lander cannot rise while matched and arrives slower than orbital every time. Reaching it wants two burns and a phase. The orbit check had already been deleted for the same reason, and the strike check was flown on a nineteen frame window, which is the sort of fixture that ends up measuring the boot time rather than the physics. Binary, because that is what a file is on this machine. A tool to build one from readable text is a small job for another day; until then the tests write the bytes with the fields named, which reads plainly enough. ---- The buffer is a whole block, and that is not caution ---- osFileRead lands a file in Data Memory and a file is stored in blocks of 256, so reading sixteen bytes into sixteen bytes of room writes over whatever follows. It did: the first version put the buffer in front of the view tables, and the program read its state, wiped the numbers every gauge draws from, and left immediately - "finished" and back to the prompt, with nothing on the screen to say why. Four checks, each seen to fail on its own break: a state file places the lander (row 192 and twelve cells of gauge, against thirty one with no file), a matched approach docks and is paid once and not once a frame, it then rides the station a tile under it, and the same approach unmatched is a wreck. The flown strike fixture and its narrow window are gone. |
||
|
|
804dd3040e |
Docking, and a ceiling that is no longer the old screen's edge
The station can be docked with: close enough, and slow enough RELATIVE TO IT, or it is a wreck. Its speed is orbital speed, which is what the marks on the drift bar point at, so the instrument for this was on the screen before there was anything to dock with. A dock pays eighty units of fuel, once and not once a frame, and holds the lander a tile under the station until a thruster lets go - checked before the speeds are put back, or a burn would be wiped on the frame it was made and the lander could never leave. ---- And the ceiling went up, which is what made it work ---- Sixty four pixels was the whole of the sky a forty column screen had over the world's origin. It was never a fact about the world, it was a fact about the view, and the wide one starts twenty four rows higher: a lander stopped at the old line was stopped a long way short of the top of its own picture for no reason it could see. It is 192 pixels now, the top of the wide view. The station went from four rows up to twelve - about two thirds of the way from the ground to the ceiling. At four it sat exactly where anything climbing away from the surface had to pass, and being run down there is not a hazard, it is a toll: ELEVEN of this suite's flights were being run down as collateral, including the ceiling check, which has to climb past it to reach the ceiling at all. Moving both fixed all eleven at once. The height bar divides by sixty four rather than thirty two, because the band it describes is 368 pixels now and half a pixel of bar to a pixel of sky would stand 184 tall and run off the top of the forty column screen it is drawn beside. ---- What is checked, and what is written down instead ---- The wreck is flown. The fixture is narrow - the window measured 496 to 514 frames of climb and it sits at 505 - and that is said in place, along with the instruction to re-measure before believing the code is broken. It was not always narrow: at the old altitude any climb from 135 to 300 frames struck. Narrow is the right way round, because it means the station is hard to blunder into. The successful dock is NOT flown, and ninety six pad files failed to find it. That is not the search's fault: climbing spends sideways speed, so a lander cannot rise while matched. It is measured working instead - tank 100 to 180, message up, and the pair holding together one tile apart for two hundred and forty frames. |
||
|
|
922511c8a7 |
A retro thruster, at half the strength of the one that lifts
Arresting a rise meant a sideways burn and a wait for the orbit to come back round. That is how a rendezvous really is flown and it is a lot to ask of somebody who has not flown one before, so down now makes the correction directly. HALF THE STRENGTH, deliberately: one sixteenth a tick against two, which is exactly gravity's own step. So it can stop a climb and it can hurry a descent, and it can never turn a landing approach into a crash faster than simply letting go would - the cheap way out of a mistake stays the expensive one. Four against eight on a keyboard, which is the same ratio. It does NOTHING to a lander on the ground, and that guard is load bearing rather than tidy. touchdown has already had its say and returns early once a lander is down, so there is nothing underneath to stop it and no crash to say it happened: measured without the guard, holding it drives the lander clean off the picture and spends a fifth of the tank doing it. ---- And the fixture that hid all of that ---- The first version of the landed check passed just as happily with the guard deleted, and the reason is worth writing down. A lander that has landed WAITS to be told its message has been read, and the keyboard fixture pads with NULs, which are not keys. So the program sat in that loop for ever and the picture was frozen at the moment of touchdown - every thruster held afterwards did nothing, which reads exactly like a working guard. The pad now presses A to get past the wait before the check tests anything, and both the row AND the fuel are read, because an engine that fired and moved nothing would keep the row and one that moved the lander for free would keep the fuel. The altitude bar check upstream leans on that same freeze for its stable end, and its comment said "stays landed" when what it means is "is not running any more". Corrected, because that is the sort of comment that sends the next person looking in the wrong place. |
||
|
|
800f555ffb |
A station in orbit, going round and not yet dockable
S1 of the station: it exists, it orbits, it wraps, it is drawn. Docking is deliberately not here - the point of stopping at this rung is to fly up and find out whether matching a four pixel a frame target feels good before any rules are written about what happens when you reach it. IT NEEDS NO PHYSICS OF ITS OWN. A body at 64 sixteenths is exactly what a circular orbit is under the rules already here: the pull and the swing cancel at that speed at ANY height, because gravity never falls off and the moon is a cylinder that does not rotate. So the station is a position, a constant, and the same fourteen bit wrap the lander uses. That also means there is no prograde or retrograde to choose - nothing privileges a direction, which is what makes a second station going the other way a thing that can exist later. Four rows above the world's origin: off the top of a forty column screen and comfortably inside a wide one, so it is somewhere to go that the zoom is needed to see. It starts half a moon away and a lap is 256 frames, a little over four seconds, so it has to be found but will not stay lost. Its column is the middle of the screen plus how far round it is from the lander, wrapped to fourteen bits - which measures the long way round whenever it is behind, so anything past the half way point becomes a negative offset instead. Off the edge needs no test at all: 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. Blue, and that is not taste. White is counted to find the ceiling warning, cyan to find the landing pads, magenta is the instruments and yellow is the lander. Blue is the one ink no check measures, and picking a measured one has broken a test twice already. toPixelsSigned is factored out of showLander, since the station wants the same sign-extended conversion. Four checks, each seen to fail on its own break. Measured going round at 244 pixels in sixty frames, off the far side of the moon, and back on the other edge. |
||
|
|
32559ce872 |
The room a zoom buys goes to the sky, not to the moon
Zooming out drew fifty rows DOWN from the world's origin, which put exactly the same sky on the screen as before with twice as much moon under it. Measured: 47 per cent rock zoomed in and 71 per cent zoomed out. A zoom that shows you more of the thing you cannot fly through is not worth a button. The eighty column screen is fifty rows and the flyable band is thirty - the ceiling is eight rows above the origin and the deepest valley is twenty two below it - so the twenty rows a zoom buys have to go somewhere. They go above. The wide view starts twenty four rows over the origin, the ground sits near the bottom, and the whole band is on the screen: 23 per cent rock instead of 71. Which needed three things. The moon is drawn from row minus twenty four rather than from nought, because rows above the origin are sky by definition and because whatever the shell left in them is otherwise still there. The row origin comes out of the view block like every other screen number. And the lander's own Y moves with the view, which meant making toPixels' answer SIGNED at last: it masks to twelve bits, so a lander above the origin came back as a large positive number rather than a small negative one - harmless while such a lander was off the picture either way, and wrong the moment the view moved up to include it. That is the point of the button. A lander at the ceiling is off the top of a forty column screen, which is where the orbit lives and why the altitude bar had to exist; zoomed out it is at row 149 and you can watch the whole orbit. Four checks, and the proportion is deliberately not on its own: measured alone it passes for a moon floating over a void, because pointing the view back at the origin leaves the rows under the terrain simply never drawn, and black counts as sky. Both breaks that matter went straight through it. What catches them is that the ground has to reach the bottom of the screen and the sky has to be empty - the latter only on a shell scrolled a hundred and eighteen lines deep, since that is what it takes to get anything into the rows the wide view moves into. The tap fixture also moved to frame 100. Fifty rows of moon take longer to draw than twenty five, and a six frame tap at frame thirty now lands before the program is reading a controller at all, which reads exactly like a button that has stopped working. |
||
|
|
437ddf8ebe |
z zooms too, so a keyboard is not shut out of it
B was the only way to swap the view, and the game is meant to be flyable without a controller - the arrows fly it when there is no pad. Somebody without one had no way to zoom at all. Tested ABOVE the pad test rather than beside the arrows, and that is the distinction: which way the lander is flown is a question a controller answers better, so the arrows stand aside for one. How much of the moon is on the screen is not that kind of question, and a player with a pad may still have a keyboard in front of them. No edge to remember here either. The console delivers a key ONCE, which is the whole difference between a key and a held button. The check puts the z forty bytes into the keyboard file, because the console hands over one key a frame: a z two hundred bytes in is a z two hundred frames away, which is past the end of the capture and reads exactly like a key that does nothing. It cost a wrong answer first time. |
||
|
|
a02d701efe |
Two zoom levels on a button, which the device already had
Forty columns and eighty are the same map, the same 8x8 cells and the same engine; only how many of them fit differs. The map is 128 by 128 either way and the moon is exactly 128 columns of it. And the front end scales whatever it is handed by the largest whole number that fits, so 320 by 200 at four times and 640 by 400 at twice fill the same glass. So the two modes ARE two zoom levels and nothing in the video device had to change to get them: forty columns shows under a third of the moon at twice the size, eighty shows nearly two thirds. Out for the orbit, in for the landing, and B says which. What did have to change is every screen coordinate in Lander, because the middle of the screen is 160 on one and 320 on the other. They now live in one block that setView copies over from whichever of two tables matches the mode, so a gauge reads a variable and never has to know which screen it is on. Several coordinates became sixteen bit on the way, since 620 will not go in a byte. follow already read HalfScreen and showLander already writes the world position straight through, so the lander itself needed nothing but its resting column. The moon is redrawn on a swap because it is filled as many rows deep as the mode shows, and a moon drawn 25 deep on a screen showing 50 floats over nothing. The swap is edge triggered. A pad is LEVEL and not an event, so a view that swapped while B was down would swap sixty times a second - which is not a zoom, it is a strobe, and it redraws the whole moon each time. The check for that holds the button for three hundred frames and requires the lander to land where a six frame tap left it; with the edge dropped it ends seventeen pixels adrift, which is the strobe costing it frames. Four checks, each seen to fail on its own break. The README's Lander entry also still described the relief orbit that the previous commit replaced, and now describes the one that is there. |
||
|
|
f5642f52b5 |
A ceiling that pins rather than ends
Climbing made a sixteen bit height count down past nought and round to 65535, so a lander that kept going came back through the bottom and hit the ground FROM ABOVE. Two thousand pixels of climb, which a full tank reaches easily. Pinned instead, and told so in the window. Leaving upward is RECOVERABLE - gravity is always there and a lander with fuel can always come back - so ending the run would punish a state the player can fly out of. What kills you out here is running dry a long way from the ground, which is a death somebody flew into rather than one a boundary handed them. TWO DIFFERENT LINES, and both were got wrong before they were got right. The warning covers being AT the ceiling or above it: compared against the ceiling itself, a pinned lander read as back inside the world the next frame and the warning was written and wiped sixty times a second, so it never appeared at all. The pin covers being STRICTLY above it: including the ceiling dragged the height back every frame and the lander could never descend, which is a lid nobody can leave and worse than the wrap. And only the climb is spent, never the fall. Zeroing the speed outright pinned it there for ever - gravity adds once a tick and a clamp running every frame wiped the pull nine times out of ten. The orbit check is gone, and the reason is in video.sh. Every window where the difference showed turned out to be a few frames wide: hold the thruster and both landers are pinned with their climbs spent, ease off and both land and freeze. A version of it passed against one disk and failed against another, which is a check measuring the boot time rather than the physics. Orbit is verified by measurement and said to be so, rather than left looking tested. cosmosLanderDry wants seventy million cycles now instead of forty: it spends the whole tank at the ceiling before it falls the length of the world. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a069ee7a00 |
Orbit, and two bars that answer instead of reporting
Going sideways lifts the moon off you. Not because gravity weakened - because at speed the surface falls away underneath as fast as the lander falls towards it, which is what an orbit is. A GRADIENT OUT OF INTEGER ARITHMETIC. Gravity is one sixteenth of a pixel a tick and there is nothing between that and nothing, so it cannot be scaled down. Instead four times the sideways speed goes into a byte every tick and the tick's gravity is skipped whenever that byte carries: the fraction cancelled is the speed over 64, smoothly, with no multiply and no divide. At four pixels a frame it carries every time. That is the linear approximation; the honest one is the square, and wants a table. It did nothing at all for its first two versions. Once because the relief was a 256th a tick, so orbit wanted a speed no lander would reach; and once because A IS THE HIGH HALF of the shift register, so multiplying by four left the answer in A while the code read B, which is nought. The same trap as the scroll register and the pixel conversion before it. And a bar for the vertical speed beside the one for drift, both GREEN WHILE A LANDING WOULD SURVIVE AND RED WHILE IT WOULD NOT. That turns two numbers into one question - can I put down - and answers it at a glance. WHAT THIS COST: the flown delivery check. A recording is a list of buttons and not a flight, so replaying it under different gravity flies somewhere else; the delivery became a crash two columns short. The fixture is still there and is still a faithful record of what somebody did, and is no longer a record of what happens. That is the standing cost of a flown fixture, and it is worse than the transcript tests dropped earlier: those broke when an output moved, and this breaks whenever a NUMBER moves. Making the delivery reachable without flying - a way to start already carrying, or at a chosen base - is what would fix it properly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
caf5e1f99d |
A base speaks in the window, not into the world
Two bugs with one cause. The console draws into the map, so a message printed while flying was a message the lander then flew over - and printing scrolls, so every one of them moved the whole world up a row. The window is at a screen position and forty cells wide, and neither is true of it. So the window is two rows now: the gauge, and whatever there is to say. The letters are ordinary tiles, because the character generator starts at the space and glyph n is character n less thirty two. The rest of the row is blanked after every message, or a short one would leave the tail of a long one behind it. Opening the throttle wipes the line, because a message that outlived the moment would be read as describing this one. The crash still goes to the console, deliberately: it is the last thing the program says and it should survive the program. A or Start continues from a message as readily as a key does. Somebody flying on a controller should not have to reach for the keyboard to say they have read something. AND TWO TESTS WENT WITH IT, which is the interesting part. cosmosLanderSoft and cosmosLanderPadOne asserted on lines in a transcript, and the lines moved off the console - so both went on passing while checking nothing at all. A test that asserts a side effect rather than the thing itself is always one refactor from being decorative. What they were for is now checked in the picture, where the message actually is. The lander check moved earlier too. The window grew to two rows, so by 1.5 million cycles the lander had climbed behind the status bar - the window doing exactly what it should, and leaving the check counting six pixels of a forty pixel lander. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bfb515e23b |
Cargo: bases with names, and a landing that is not an ending
Four bases, told apart by the scheme their pad is drawn in, so "the cyan one" is a thing a person can say and a thing the machine already knows. Yellow is missing on purpose: it is the lander, and a base the same colour as the thing landing on it would be a poor joke. Land empty at a base and it loads cargo for the base ACROSS THE MOON, two along - so the pairs are cyan with red and green with blue, and the wrapping surface is a route rather than scenery. Land carrying at the right one and it takes the cargo and pays eighty units of fuel. Land at the wrong one and nothing happens, which is why the destination will want to be on the screen. A LANDING NO LONGER ENDS THE RUN. The lander rests where it is, exactly on the surface with both speeds zeroed, until the throttle opens again - which is the only way to stop being landed. Gravity does not pull on something already sitting down, and a base does not hand out cargo sixty times a second to a lander parked on it. The pad array holds the base's number plus one rather than a flag. Nought still means no pad, so it is still one lookup, and a flag would have to be followed by "and which of the four" - the same walk done twice for an answer already in hand. Pads are eight columns rather than four. Four was 32 pixels in a moon 1024 round, which is a target somebody flying by feel misses over and over. WHAT IS NOT COVERED, and why: the delivery and wrong-base paths need a lander flown from one base to another, and hand-authoring a recorded pad input that hits an eight column pad across a 128 column moon is a piloting exercise rather than a correctness one. Several attempts got within two columns. Loading, crashing, landing off a pad and running dry are all covered; delivery is built and flown by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7257ad369c |
Landing pads, carved rather than looked for
A random walk does not leave flat ground and a lander wants some. Four pads are cut into the moon after it is generated, each four columns levelled to whatever height its first column happened to have - so they sit in the landscape rather than on a shelf above it. The moon decides where they are; this only decides that they are flat. Searching for flat spots was the alternative and it can fail, which means a fallback that carves anyway - the carving, plus a search nobody needed. They are marked by an ATTRIBUTE and not a tile of their own, which costs no art at all: a nibble is added to every index in a tile, so one solid block is grey moon or a cyan pad depending on the byte beside it. Which columns are pads is an array, because asking has to be one lookup. Four comparisons per column per row is 12,800 of them for one screen, and the landing verdict asks the same question again. THE LANDER STARTS ABOVE ONE, because that is where a porter's day begins. Starting in the middle of nowhere meant a straight descent landed in the middle of nowhere, which is a fine thing to be able to do and a poor thing to have to. That change cost the crash test its teeth, and the way it did is worth keeping. It held nothing at all and let the lander fall - and a short drop onto the high ground of the base you started above is survivable, which is correct, and left the test saying nothing. It holds Right now: lateral speed has no limit and nothing slows it, so a slide always ends badly however the rest is tuned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
6073086584 |
The system takes a leftover window down
Lunar Porter put a fuel gauge up and never took it away, so the shell came back with FUEL across the top and the cursor underneath. Clearing did not help: a window is a layer at a SCREEN position that does not scroll, which is exactly what makes one left behind unpleasant - it sits over whatever comes next and cannot be scrolled off, cleared away or typed past. Taken away rather than given back, like the sprite table and for the same reason: nothing the shell draws is a window, so there is nothing to restore. And a program that FAULTED while one was up could not have taken it down itself, which is why this belongs to the system rather than to whichever programs remember. The check for it needed writing twice, and the first version was the familiar kind of wrong. It counted the gauge BAR's colour - and the bar disappears on its own whatever happens, because it is drawn with a tile the screen save puts back, so the check passed with the teardown deleted. What actually survives is the LABEL, in font tiles the shell needs anyway. So the screen is cleared afterwards and read cell by cell. With the window down a cleared row is "> " and a cursor; with it up the same row is F, U, E, L. Cells one and three being empty is the whole difference, and it is 29 and 22 pixels of it rather than a threshold somebody has to believe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2274be4b68 |
Lunar Porter, rung three and a half: fuel
Every thruster costs a unit every tick it fires, so holding two at once costs two - the honest price, and it makes a drift you corrected expensive in a way a drift you avoided is not. AN EMPTY TANK IS NOT AN ENDING. There is no message and nothing stops: a lander with no fuel is still flying, it just cannot do anything about where. What happens next is gravity, and gravity is patient. The test for it holds the thruster from the first frame to the last and crashes anyway, which is what says the fuel is real - a lander that could hold Up for ever would land every time, and the economy this is the first half of would have nothing to buy. The gauge is in the window, which is what the window was built for two commits ago: a bar at a SCREEN position, so the moon turning underneath does not carry it off. Thirty five cells after a label, redrawn whole every frame because seventy bytes out of one port is cheaper than working out which of them changed. A byte of fuel, and a byte is enough. Over eight it is a bar of up to thirty one cells - a shift, because there is no divide - and at a unit a thruster a tick it is about forty seconds of holding the engine open. Sixteen bits would be more arithmetic for a number nobody reads to the unit. Cargo and the bases are the other half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
df50c2f0f8 |
The pad was working; the game was told there was not one
0x64 counted only the RECORDED pads. So a controller plugged into Voyager reported its buttons perfectly, and every game asking whether there was a controller was told no - which is exactly what Lunar Porter asked, once, at startup, before falling back to the console for the rest of the run. The cause is worth naming: a front end calls padSet every frame for every pad, so "held nothing" is the commonest thing it says and cannot also mean "there is no pad here". Connected is said separately now. Pad nought is always there behind a window, because the keyboard is behind it - which is the useful answer rather than the literal one. And Pad.asm, which is what should have existed before any of that guessing began. It prints a line whenever a pad changes, and tells apart the three states that look identical from inside a game that will not respond: one nobody noticed, one mapped to nothing, and a mapping that is wrong. WHY A PROGRAM AND NOT A PRINT IN THE FRONT END: because the question is what the MACHINE can see. A front end reporting what it thinks it is sending answers a different question, and the gap between those two is the whole of this bug. It also found that osPrintNumber takes A as the HIGH half - the same way round as the shift register and every other pair here, and not what a byte in A wants. Every value came out 256 times too big. Gravity is one frame in ten rather than six. The ratio between thrust and gravity is the feel; how often the tick comes round is how fast that feel arrives, and one in six was still touchy. Same lander, more time to think. And the verdict waits for a key. It printed and left immediately, taking the screen with it - so the one thing worth seeing, the lander sitting on the ground it had just reached, was gone before it could be looked at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8eb4e4d67e |
Lunar Porter, rung two: it lands, or it does not
The terrain is an array in Data Memory rather than something read back out of the map, and that is the whole reason this is cheap: the ground under the lander is one index into 128 bytes, where asking the screen would be a transfer through the controller every frame. The column is the world position over eight, masked to the moon's 128. The surface is that column's row times eight - three turns left of the shift register, since a row is at most 24 and 192 fits in the low half. The feet are the lander's top plus its eight pixels. WHAT DECIDES IS THE SPEED AT THE MOMENT IT ARRIVES. Both of them, and both have to be gentle: three quarters of a pixel a frame downwards and half of one sideways. Sideways is the tighter on purpose, because a landing that was soft downwards and sliding is a lander on its side - which is the interesting half of the difficulty, and the half the drift bar was blind about until it existed. Two fixtures say it works, and they differ only in what was held: one holds nothing and falls the whole way, the other pulses the thruster six frames in sixteen and survives. Same terrain, same seed, same keys. Also: the gamepad did nothing, and the reason is that the four direction buttons are the D-PAD. A lot of controllers made this century have one nobody uses - the thumb goes on the stick, which reports as an axis rather than a button - so a pad that was plugged in and working correctly did nothing at all. The stick counts as held past halfway now. Untested here, because there is no controller in this environment and the suite runs headless; Voyager also says at startup which controllers it can see, so a pad that still does nothing can be told apart from one nothing noticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
db0c26e13f |
A bar for the drift, and a lighter touch sideways
A moon has no air, so a sideways drift never stops by itself and stopping one means cancelling the velocity EXACTLY. That is not hard to do; it is hard to do blind, which is what it was - a number nothing on the screen said anything about. So sprite one is a bar whose width is the drift. It runs right from the middle of the screen for a rightward one and left for a leftward one, so which way is as plain as how fast, and stopped is the one state with nothing drawn at all. The whole of it is a target width written once a frame; the device stretches one tile into it and the program draws nothing. Sideways thrust is one a tick rather than two. At two, the smallest correction available was twice the size it needed to be and overshooting was the normal outcome. WHICH ZERO MEANS NOTHING TURNED OUT TO MATTER. A target width of nought is the NATURAL width, not an empty sprite - so a bar with no drift in it came out eight pixels wide, sitting at the middle of the screen, saying "stopped" in the same shape it says "drifting slightly". What draws nothing is a SIZE of nought, which is the other zero in the other byte. Both meanings are deliberate and documented and it still caught me out inside a week of writing them down. The check for it earned its place by failing on that before it was found, which is the best evidence a check can offer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b1dde7908c |
Lunar Porter flies on a controller
A held thruster burns every tick it is held for, which is the whole reason the pad exists: the console can only say a key went down, so a thruster driven by it could be pumped and never leaned on. The burn happens on the same tick gravity does, and for the same reason - a sixteenth of a pixel is the smallest step this arithmetic takes, and applied sixty times a second it is an enormous acceleration. On the tick, thrust and gravity are two numbers whose RATIO is the whole feel of the thing. Position still moves every frame; only the acceleration is stepped, and nothing can see that. Two against gravity's one, so climbing and falling are the same speed. Three was the first try and it left the moon after about a second of holding. If there is a pad the console's arrows are ignored, because under a window the same keypress reaches both - the pad as a level, the console as a byte - and a thruster that fired twice for one press would be a mystery to anybody tuning it. q still quits, since a pad has no letter for it. With no pad the arrows still burn once a press, which is the most that can be done down a wire. And break.sh now rebuilds the disk images as well as the binaries. Half the things worth breaking here are SplitBit assembly rather than C, and those live on the fixture disks - so an edit to a .asm file changed nothing the suite could see, and the tool reported that nothing caught the break. That is the exact lie it was written to prevent, turning up in a new place. With the disks rebuilt it catches this one: the lander falls between the two captures instead of climbing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d6c81fa32c |
Lunar Porter, rung one: it flies
A lander over a moon that wraps. Landing, crashing, fuel, cargo and bases are not here - this rung exists to answer whether it FEELS right, because everything after it is bookkeeping and none of it is worth building on a lander that is no fun to fly. The moon comes for free. The map's column origin is a ring in hardware, so 128 cells is 1024 pixels of surface with no edge and no seam to cross. Position and velocity are sixteen bit in SIXTEENTHS OF A PIXEL, and the unit is the design: gravity is a small number added to a velocity and a velocity is a number added to a position, with no multiply or divide anywhere. 1024 pixels is 16,384 sixteenths, which is 2^14 - so going all the way round is an AND with 0x3FFF rather than a comparison, and it is never wrong at the seam. The lander never moves sideways. The world scrolls under it and it sits at the middle of the screen, which is a byte a frame instead of two and is also what makes the wrap invisible: there is no moment where it jumps. One key is one burn. The console says which key went down and there is no such thing as a key coming up, so a thruster cannot be held - a press adds to the velocity once. That is a property of the machine rather than a choice this program made, and it reads as pumping the engine. Four bugs found by running it, all worth keeping written down: B CANNOT BE A LOOP COUNTER here. Every comparison is an INIB, so the count was overwritten by whichever bound was last tested and the loop reset itself for ever. Counters that outlive arithmetic live in memory. A subroutine answers in Q, and the AND after it read A. The moon came out flat because it was testing the height against 1 instead of the random number. Row minus height, not height minus row: they are equal at the surface, equal does not borrow, and the surface row has to be ground. And the shift register has A as its HIGH half. Written the other way, the view scrolled by 256 cells for every one it should have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
5222c85100 |
More fills the screen it is on, not the screen it was written for
Twenty two lines was right when there was one screen size. It still is on the forty column screen and wastes three fifths of the eighty column one, so More asks the rows register instead - which is readable for exactly this sort of reason. Rows minus three is twenty two on a twenty five row screen, so nothing changed underneath anyone already reading files this way. It fills a bigger screen and leaves a smaller one alone. A bitmap screen has no rows and says so with a nought, which through an eight bit subtraction would be 253 lines. Anything under five falls back. The existing test stopped testing paging the moment this worked: 32 lines fits in a 47 line page, so the file never paged and the recording lost the prompt entirely. The fixture is 60 lines now - the INPUT needed moving, not just the output, which is the failure this project keeps meeting. And cosmosMoreNarrow, which runs Mode first and pages the same file on the forty column screen. Two recordings of one file at 47 lines and at 22: a More that went back to a constant would make them the same length. Both were verified with break.sh, and the first attempt was a bad break rather than a bad test - it replaced one of two reads of the rows port and the other still fetched the real value. Which is a fair argument against reading a port twice, so it is read once and kept now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fba1b553d2 |
Depth: a ball behind the near pillars and in front of the far ones
The demo for what V5 added. Four pillars at four distances, each ONE 8 by 8 tile stretched to its own width and height, and a ball walking past all of them at a distance between two. What it shows is the thing an ordering cannot. The ball is sprite NOUGHT and every pillar is numbered after it, so table order puts the ball in front of all four - and it is still hidden behind two of them, because the depth buffer is asked per column. Caught mid-straddle in the checks: the ball is 48 wide and the pillar 32, so it shows on both sides and nowhere across the middle. Writing it found the conceptual trap in the feature, which is now written down where somebody will hit it. The pillars first carried their own distance in their entries AND wrote that same distance into their columns, so each was asked whether it was in front of itself - and 20 is not nearer than 20, so all four vanished. THE BUFFER IS WHAT HAS BEEN DRAWN AND A SPRITE'S DEPTH IS A QUESTION ASKED OF IT. Scenery writes it; it does not ask. Also found that a scheme only gives a colour to index one. The default palette sets each scheme's paper and ink and nothing between them, so art drawn in index two comes out black until a program writes a palette. And the fixture disk's root directory was full: four blocks, 32 entries, all taken, so adding an app failed the whole disk build. Loudly, which is the right way round - but it is a wall that moves for free, so it is eight blocks and 64 entries now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
cb898450b5 |
Sprites that scale, and a depth buffer to hide them behind
A target size in PIXELS rather than a multiplier, which is the whole of why this is usable here. A billboard at distance d wants to be k/d pixels tall, and that is a number a program has anyway - out of a lookup table, most likely. A multiplier would have to be a fixed point fraction arrived at by dividing, and this CPU cannot divide. Zero on an axis means the natural size, so every sprite written before scaling existed still means what it meant. The two axes are independent, and that shape - one tile wide at its own size, stretched to whatever height a distance says - is a wall column in a pseudo-3D game. Measured: a DDA step costs 85 cycles, so 80 columns of ray casting is about 85,000 cycles, or 12fps. Drawing those walls from the CPU instead would be 256,000 writes, fifteen frames of cycles for one frame of screen. The device doing the pixels is what makes such a game possible at all here, not merely faster. And a depth buffer, one byte a screen column at 0xD000, written by the program. A sprite with a depth draws only in the columns it is in front of. PER COLUMN, and that is the point: a billboard is nearer than the wall at one end of itself and further at the other, and no ordering of the table can say that. Table order settles sprites against each other; the buffer settles them against the scenery. Zero means no test at both ends, so a program that never writes it behaves as it did before it existed. The entry grew from 8 bytes to 16 - now, while two programs use the table, rather than once a game is written on it. Bytes 0 to 7 kept their meanings, so Sprite.asm needed no change. The pass is rewritten to walk where a sprite is GOING rather than where it came from, which is what makes a stretch and a squash one operation. It also made flipping fall out: turning the source coordinate round mirrors the tile order and the pixels inside each tile in one step, where drawing tile by tile had to be told to do both. All 111 checks passed unchanged at natural size, which is what says the rewrite changed nothing it should not. Clipping moved out of the inner loop and had to: a target size is sixteen bits, so a sprite asked to be 60,000 pixels tall would have been sixty thousand turns of a loop that drew eight rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
9eed23120f |
Four pages of tiles, in bits that were already there
A tile number is a byte and a byte reaches 256, which is not many once a font has taken 135 of them and a game wants a character, a background and a wall. Bits 4 and 5 of the attribute now say which page of 256 the number is in - bits already written on every cell and every sprite, and reserved for this since the attribute was defined. Four pages of 16K is 64K, which is the whole atlas, so THE FOURTH PAGE IS THE MEMORY THE SPRITE TABLE AND THE PALETTE ARE IN. That is not a hole in the design; it is the answer shared video memory has always given, and it is checked rather than forbidden. The atlas is 1024 tiles, and what a program spends on sprites and colours comes out of them: no sprites means page 3 is art, and sprites means 768 tiles and a reason. The page is a property of the CELL and not a mode, so one screen shows tiles from all four at once and nothing has to decide which page it is in. Both places a tile is drawn from now ask one function where the art is. They would otherwise drift: the sprite pass was written days after the map pass and neither is where the other is looked at. Nothing in CosmOS changes. The shell draws from page 0, which the screen save covers; a tile left in another page is invisible unless a map cell names that page, and the map is given back or cleared. Both breaks were tried and both failed the checks - and the second had to be tried twice, because the constant it needed lives in video.h and the harness was only editing video.c. That is the same silent no-op as yesterday's uncompiled break, in a different disguise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a916103a7f |
Sprites: things that move without the screen moving
Everything drawn on this machine was in a cell. Something between two cells meant rewriting both; something moving a pixel at a time meant rewriting them sixty times a second, which is affordable for one thing and not for twenty. A sprite is put at a pixel and the device draws it over whatever is behind, so moving it costs two bytes. MADE OF TILES, which is the decision the rest follows from: m by n taken in reading order from one index, so there is no second pixel format, no second kind of memory, and nothing a sprite can show that the map cannot. A 16 by 16 character is four tiles and the background can name the same four. 256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry address is a shift, the same no-multiply argument as the palette's four. Position is signed and sixteen bits, because 640 by 400 does not fit in a byte and a sprite has to be able to sit half off the left rather than appearing whole at the edge. A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested before the attribute is added, so a hole belongs to the art and not to the colour scheme. The same rule the other way round is what "behind" means: drawn only where the background pixel was zero, so a thing walks behind a pillar and in front of the floor in one frame. All of them draw, every frame, so they cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers; this has a loop. The limit is the size of the table, which is a constant rather than a property of what is on screen. And the system takes them down at exit. The sprite table sits in the gap the screen save walks around - to the end of the map, then the palette - and that is right, because nothing the shell draws is a sprite: there is nothing to give back, only something to take away. Otherwise a program that put a ball up and left would leave it over the prompt, in front of everything, with nothing able to type it away. Sprite.asm deliberately leaves its own, because a program that faulted could not have cleared it. Every check here was re-broken and failed: transparency, reading order, draw order, priority, and size. Size needed breaking twice - the first attempt did not compile, and a silent build failure had left the old binary passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
eee95ef0ce |
Flip says the true thing, and the checks that let it lie
Two bugs, both in what the demo claimed rather than in the device. The assembler has no string escapes, so the "\n" written in a literal printed as a backslash and an n. A newline is a byte; Say.asm has always written one as 0x0A 0x00 and this now writes it out of the console port. And the line whose whole job was to still be there afterwards was wiped out on the way back, because the program called osTakeScreen - which restores the screen AS IT WAS BEFORE, so the tidy-up erased the one thing the demo was pointing at. It did not need saving: nothing it touches is the shell's. A program that damages nothing should not ask, and asking anyway costs it the screen it was standing on. Which turned out to be untrue as written, and that is the third thing. Flip drew with a tile of its own, and the system copies the font back over every tile at exit - so the filled screen went blank the moment the program left, and the check that the system put the display back could not tell a restored screen from an abandoned one. It passed with the restore deleted. So did the check that a program can show the other screen at all: a blank screen counts as one colour just as well as a filled one does. Now it fills with 0x0A, which is an asterisk in one of the reversed colour schemes: paper is the colour and ink is black, so a whole screen is drawn with NO TILE REDEFINED and it survives leaving. Both checks ask for the commonest colour in the picture rather than counting colours or naming a pixel - the font's only blank glyph is the space, whose attribute nibble is nought, so a filled screen is always a pattern and which pixel lands on paper depends on the character. Both were re-broken afterwards and both failed this time. Also cosmosFlip, a transcript test, which is what would have caught the printed backslash in the first place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
023362b05a |
A second screen, and one port to say which is shown
A screen drawn where it can be seen is seen half drawn. A program that moves forty things and rewrites the map underneath them is wrong for as long as it takes to put them all right, and at a megahertz that is long enough to look at. So the device brings a second screen bank, on port 0x3B, and port 0x3C says which of the two is displayed. Everything a program draws into the other one is invisible until one byte shows the whole of it at once. ONE REGISTER IS ENOUGH, where the hardware this imitates needed two. The other said which screen the CPU's window pointed at; there is no window here, because a program reaches a bank through the memory controller by its number. Writing to the screen that is not shown is a matter of naming its bank, and the device never has to be told. And a flip cannot tear: a frame is drawn from one bank in one go, so a flip either happened before that frame or happens before the next. There is nothing to race, where the real machines had to catch the few lines between frames to swap in. The console draws into whichever screen is displayed rather than one of its own, so a fault message lands where somebody can read it even if a game had flipped. And CosmOS puts the displayed screen back at exit, the way it already puts back the cursor and the ink: a program that faulted while flipped could not have, and a shell that only came out right for programs which remembered would come out wrong the day one crashed. Flip.asm is the worked example. It deliberately does NOT restore the display itself - that is the point of the paragraph above, and it is what makes the system's guarantee the thing under test rather than the program's good manners. Written the other way round first, where it passed with the guarantee deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2abc8281df |
Loops, and the scripting language is a language
while and for. Both only mean anything in a script, because a loop goes back to the line that opened it and a prompt has no line to go back to - and both say so rather than doing something surprising. THE SCRIPT READER KEEPS THE POSITION OF EVERY LINE before reading it, which is what makes any of this possible: by the time a line has been read the reader is past it, and a line is not a fixed size to subtract. Three words per line, and the block is read again on the way back so the pointer into it means what it meant - the same thing nesting one script inside another already did, for a different reason. THE TWO LOOPS END DIFFERENTLY, and that is the design rather than an accident. A while is taken away at its end and its own line asks the question again, so nothing has to be remembered. A for is not: how many words it has used is kept in the block, and its line reads itself again and counts one more off the front. That is a byte in a block instead of a copy of the word list in every one of them. Blocks grew from a byte to a record of sixteen - state, kind, words used, and where the line that opened it was - and sixteen because A and B are a shift register, so four rotations turn a block number into its offset. The history and the variables are addressed the same way for the same reason. Nested loops, an if inside a loop, a loop inside a branch nobody takes, and a for with no words: the last two run no times rather than once, which is the case worth having a test for. Three things found by running it: textSame asks whether two WHOLE strings are the same, so "in red green blue" is not "in". The word has to be split off before it is compared. A for typed at a prompt complained about while, because both arrive at the same place. One message that names neither is better than one that names the wrong one. And docs.sh caught a naming convention nobody had written down: it recognises a packed name by its label ending in "Name", so ForName2 was silently not counted. It failed the right way round - saying the run was shorter than the count claims rather than passing - but the convention now lives where the names are and not only in the checker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
16f8232a35 |
Lines that are only run sometimes
if, else, end, and same. IF TAKES A COMMAND, which is one rule rather than two and is why comparing values needs no syntax of its own: "same" is an ordinary command that fails when its two words differ, so "if same $a $b" falls out of the rule instead of being an exception to it. Anything else that can fail is a question too - "if load Snake.sbx" is a perfectly good one. The shell already had the other half. LineFailed exists because a script stops at the first line that did not work, so every command was already saying whether it had, for a different reason entirely. A BLOCK HAS TWO KINDS OF NOT-RUNNING. One where an else would turn it on, and one where it would not - which is what an if pushes when something above it is already being skipped. That is what makes nesting need no looking down the stack: the top of it says everything. A branch nobody is taking is not even looked at. The skipping happens BEFORE the names are filled in, so a variable mentioned in a branch that is not running is not an error - a line nobody runs must not be able to fail. AND LINES MAY BE INDENTED, which they could not be before there was anything to indent inside. Nobody writes an if inside an if without indenting what is in them, and a leading space used to make the first word empty and match nothing. Found by writing the test script the way anybody would write one. CALL commandFailed became BRI commandFailed in nine places. It never returns - it marks the line and branches to the prompt - so calling it was a lie that cost a Stack frame each time, and fourteen other sites already branched. THE LINT RULE FOUND THIS, three days after I wrote the rule and on my own code: two false positives that were really the linter being right about a CALL that is not one. It does not fix the leak on its own, since a failure inside any called routine still abandons that frame, but it removes the cause of the commonest case and makes the code true. The mechanical edit then left a BRI prompt stranded behind one of them, and the linter caught that too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a8707f29f0 |
Names for things
"set apps /Apps", and then "$apps" anywhere on a later line stands for it. A name stops where a name stops - letters and digits - so it composes into a path without anything having to be quoted, which is the whole reason a script would want one. THE SUBSTITUTION HAPPENS ON EVERY LINE THE SHELL IS ABOUT TO RUN, typed or read out of a file, so the two behave the same and no command below has to know that variables exist. Same shape as the line editing: one place the whole system already flows through, rather than a decision made twenty times. A NAME NOTHING WAS SET TO DOES NOT RUN THE LINE. Every other shell expands it to nothing, and that is the wrong answer here: a mistyped name would quietly become an empty path, which is the class of silent wrong answer the rest of this system spends its effort refusing. It says so and the line counts as failed, which stops a script - and the test proves that by running one, where the line after it must not appear. Somebody who wants an empty value writes "set name" and gets one, so the escape hatch exists and has to be asked for. A NAME TOO LONG IS AN ERROR RATHER THAN A SHORTER NAME. Cutting it off at fifteen characters was the first version, and it is the same fault wearing a different coat: two names differing only after the fifteenth would be one variable, and the complaint about a missing one printed a word nobody typed. Eight slots of sixty four bytes - sixteen of name, forty eight of value - and sixty four rather than eighty because A and B are a sixteen bit shift register, so two rotations turn a slot number into its offset. The same trick the history uses, and the reason neither needs a multiply this machine has not got. TWO THINGS I GOT WRONG AND ONE I FOUND: doSetVar ended in RET. It is BRANCHED to from the dispatch, not called, so that RET went wherever the Stack happened to point - the same fault that formatted a disk last week, in a command written three days after the rule was named. The new lint rule does not catch this shape: it fires on falling INTO a subroutine, not on a branch target that ends like one. And a test of the expansion's answer, which is dead code: commandFailed does not return. It marks the line and branches to the prompt, the way every failure in this shell is reported, so the only way out of the expansion is the one where it worked. Which turned up a real leak, measured and not yet fixed: every failure that goes through commandFailed abandons the frames between the prompt and the call. SP goes from FFFD to FE6D over twenty of them, twenty bytes each. Its own commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3c76934a9a |
Tab reaches the disk
Paths and programs, which is the half that makes it worth having. The first word of a line is a command or a PROGRAM, offered under the name somebody would type - the extension taken off - and anything after it is a file, offered as it really is. A separator anywhere in the word says which directory to look in. A directory answers with a separator on the end instead of a space, which says what it is and lets the next part be typed straight away. The answer ending in one is also what stops a space being added, so that is one test rather than a flag. PROGRAMS ARE LOOKED FOR WHERE THE SHELL WOULD LOOK to run one: where you are, /Apps on the disk you are on, and /Apps on drive 0. Offering something the shell would not find would be finishing a word into a thing that then does not work. Drive 0's is skipped when that is already the drive, or every program in it would be offered twice and nothing would ever be the only match. Walking somebody else's directory means standing in it, which is the only way to walk one here, so where the person was and which drive they were on are put down first and restored whatever happens. Three bugs, all found by running it: THE DIRECTORY TEST WAS INVERTED. dir asks the same question the same way round four hundred lines further up, which is what made it obvious once looked at. THE /Apps WALK OVERWROTE THE TYPED PATH. The whole search runs a second time to list the matches, and by then TabDir said "/Apps" - so a word that had named nowhere went looking in the wrong place and listed nothing at all. Two ways into the walk now, and the typed path is never written over. AND LISTING ONLY KNEW ABOUT COMMANDS, because it was a second copy of the walk. It is the same walk with a flag now: finding the answer and showing the matches are the same question asked twice. Also cosmosMonitor, which had been RE-BLESSED INTO MEANINGLESSNESS by the wall move. It disassembles a loaded program, at an address the input names - and that address moved a page while the recording was simply re-recorded to whatever came out, which was a page of zeroes. It is pointed at 5000 again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bb065fe221 |
Tab finishes a word somebody started
The first word of a line, against the shell's fifteen commands. One match goes in with a space after it, because a word that can only be one thing is finished. Several are folded into their longest common prefix and that goes in, which is the most that can be said without guessing which was meant - and if that adds nothing, the matches are listed and the line put back underneath. THE LINE COMING BACK IS THE HALF I EXPECTED TO BE HARD and it was already solved. The prompt has been reprinted somewhere else entirely, so the editor's idea of where the line begins is wrong - but editAnchor works that out backwards from where printing ended, precisely so it survives the screen moving. Listing is a redraw it already knew how to do. editInsert became editPut, a routine, because completing a word puts in several characters and every one of them is that. Which cost a bug immediately: the old inline code left the insertion point in A, and a RET puts A back to what the caller had. Two more bugs worth naming, both mine and both the same shape - a pointer that had moved: THE CANDIDATE'S START HAS TO BE KEPT. The comparison walks DP3 through the name as it matches, so by the time a match is declared, DP3 points at the part AFTER what was typed - and that is what got copied. "he" completed to "he" because the answer taken was "lp". AND THE INSERTION STOPS AT OR PAST, not exactly equal. With the wrong answer the two counters passed each other and the loop ran off the end of the buffer, filling the line with whatever was next in memory. They cannot pass each other now, and the branch stays, because the cheaper failure is worth nothing. MY OWN TEST HAD A HOLE and breaking the code found it. The later-word case pressed Tab after a space, where there is nothing to finish anyway, so it passed whether or not the shell checked which word it was on. It types "echo he" now, which would become "echo help" if it did not. The assembler's label table went past 1024 and is doubled. A ceiling reached once will be reached again, and it is pointers into source already in memory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3449405b18 |
Give the system another page of each memory
CosmOS had 1,161 bytes of Program Memory left before the address applications load at, and Tab completion is not going to fit in that with anything to spare. So the wall moves up one page: the system keeps below 0x4FFF and 0x2FFF, and an application is based at 0x5000 and 0x3000. A PAGE IS A CHEAP THING TO GIVE IT AND AN EXPENSIVE THING TO RUN OUT OF. An application still has 44K of Program Memory before the vector table and the largest one here uses 7.5K, so what was taken from applications is space nothing has ever asked for - while what the system gained is the difference between building the next thing and counting bytes while building it. Not doubling, which was the version that would have cost application space worth minding. One page, and the same again when it is needed. Nothing in the machine knows where the wall is, so this is 34 #Base lines, one threshold in the fault handler, and the table in the CosmOS README that Tests/docs.sh reads its limits out of. The native assembler's scratch map had to move with it, and docs.sh said so before anything ran: its data reached 0x40D6 and its buffers began at 0x4000, so they were sitting on its variables. That file already carries a paragraph about the floor coming up and the map staying where it was. It has happened twice now, and been caught by a check the first time wrote. Twenty three recordings are the same runs a page higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f97d15de08 |
The font comes from a chip, not from RAM that remembers
videoReset zeroed video memory and then wrote the font and the sixteen colour schemes into it, and the comment above that said out loud what was wrong with it: "everything here is ordinary video memory". RAM does not wake up with anything in it. That was the last piece of magic in this device, and it looked harmless until something wanted the font BACK - a program that redefines a glyph had destroyed the only copy there was. So the device has a character generator, the way the machines this one is pretending to be really did, and the copy into RAM is a thing it DOES rather than a state it mysteriously starts in. Command port 0x39: bit 0 for the font, bit 1 for the schemes. THE RAM IS STILL RAM. A program may overwrite every glyph and every colour and should be able to, which is what makes this a tile engine rather than a text display. What changed is that it is no longer a one way door. NEITHER COMMAND CLEARS WHAT IT DOES NOT OWN. The font used to clear the whole of tile memory before writing itself, which was harmless while it happened only at reset and is wrong the moment a program can ask: a program that defined a tile of its own and then wanted its text back would have paid for it with the tile. The reason it is a chip rather than a file on the disk, which was the other candidate: the boot chain prints before CosmOS exists. Stage one prints "?" when there is nothing to boot, and if the font came off the disk then the message about the disk having failed would be the one thing that could not be drawn. A system that wants its own font still loads one over the top - the ROM is the floor, not the policy. Two things that had been worked around now simply work. The shell asks for both whenever a program exits, so a program that redefined a letter no longer leaves it unable to spell, and Grid no longer needs to have saved the screen to avoid handing back green text on blue. And the fault screen asks for the glyphs first, because a message spelled in somebody's tile graphics is no message at all. Five video checks. Two runs each for the font and the schemes, since the map holds a tile NUMBER and the glyph is looked up when the frame is drawn - so restoring changes every cell using it, including ones drawn before, and what the two runs differ by is the command. The third guards the decision not to clear: tile 200 has to survive the font coming back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
925388c2f2 |
Keep the personal disk three starts back
Copied before every start of the machine, and kept several deep rather than copied over one file. ONE BACKUP TAKEN AT EVERY START IS WORSE THAN NONE. The way a disk is lost is that something goes wrong - and the very next thing anybody does is start the machine again to see how bad it is, which is exactly when a single backup gets overwritten by the wreckage. Three deep means the damage has to happen and then be started past three times before the copy that would have helped is gone. AND IT IS NOT GUARDED BY A CHECK THAT THE DISK STILL LOOKS RIGHT, because no such check can be written. The disk that went missing this week was a perfectly valid and perfectly empty filesystem: the format had succeeded, and there is nothing to look at that says a disk has lost anything. That is the whole reason to keep the old ones rather than to judge the new one. The backups sit outside clean's reach like the disk itself. A backup a rebuild deletes is not one. The message the disk prints when it is first made said "nothing in this makefile will touch it again", which stopped being true the moment this was added. It now says written to, and says where the copies go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
66be42d7bb |
A word the monitor does not know goes to the disk, not into the weeds
There was nothing at the end of the monitor's command list. An unrecognised word
fell off it and straight into sayPrompt - which is a ROUTINE, so its RET had
nothing of its own to return to and went wherever the Stack happened to be
pointing.
The user found it by typing a program's name at the monitor prompt, which is an
entirely reasonable thing to do: the monitor is a mode of the shell, so
everything the shell does is meant to work in it. What they got was a fault, and
before that a second prompt printed on top of the first - which is sayPrompt
doing exactly what it is for on its way past, and the tell that it had been
entered rather than called.
WHERE THAT RET WENT DECIDED HOW BAD IT WAS. Usually 0x0003, in the middle of
newLine, and the machine stopped on a byte that is not an instruction. Once it
was inside sbfsFormat, and the machine formatted the disk it had booted from -
the user's would not start again, and neither would mine, which is how I came to
have a reproduction before I had a diagnosis.
Pre-existing, and not recent: it is there at
|
||
|
|
fd9c4c75f8 |
Tell the person where it hurts
A fault stopped the machine and printed a line to standard error. On a terminal that is a diagnosis. Behind a window it is a frozen picture and no reason at all, because the message went somewhere nobody was looking - the machine looked hung and was not. It had stopped, and said so invisibly. CosmOS catches all five faults now and says what happened on the screen, with the address, in red. A FAULT ENDS THE PROGRAM, NOT THE MACHINE. That is the answer to "carry on or start again", and it is not a compromise: a bare RETI from most of these meets the instruction that failed and fails again, so carrying on was never on offer. But the machine is almost never what is broken. Everything the shell puts back when a program exits - the Stack, its vectors, the drive, the working directory, the console, the screen - is exactly what wants putting back after one dies, so the handler sets a status and joins handleExit. You are back at the prompt, and the program is recorded as having STOPPED rather than finished, because saying "finished" under a red fault message would be the shell contradicting itself. A fault below where programs load is the system's own, and there is nothing to go back to. That one says so and stops. THE SCREEN GOES BACK TO A MODE TEXT CAN BE SEEN IN, and that is the part that matters rather than the part that is prettiest. A program that faulted in bitmap mode left the console with no text rows, so it draws nothing at all: the message would be perfectly correct and completely invisible, which is the one thing it must never be. Two palette entries go back for the same reason, since a program that wrote its own colours can leave every ink the same as every paper. Only the two the message needs, so the rest of what the program chose is left alone. Both halves are checked by looking at the PICTURE, because the serial line was never where the problem was. Crash blind ruins the palette and drops into bitmap mode before it faults; without the mode the screen comes back 320 by 200 with nothing on it, and without the palette it is the right size with the message present and unreadable. Each break loses the red on its own. Crash is also a program worth having: it breaks in whichever of the five ways you name, so a fault screen can be looked at without having written a bug first. Two things found on the way: The native assembler keeps its OWN copy of the reserved vector names, so it did not know NoHandler or NoDevice and built a cosmos.bin that differed from the host assembler's. Caught by native.sh, which is exactly the drift that test exists for. And cosmosMonitor had dead input. It assembles code into 0x8000 and runs it, and that code faults - which used to kill the machine, so everything after it in the file had never run. It runs now, and the recording grew by sixty lines of monitor session that had been unreachable since the day the fault was put there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4d976fc22a |
A program reading a line gets the editing too
osReadLine goes through the shell's editor now, so anything that asks the system for a line gets arrows, Home, End and Delete. The editor is a program, and a word typed with two letters the wrong way round can be put right without starting the line again. IT DOES NOT GET THE HISTORY, and that is the interesting half. Edit would otherwise fill the history with the text of somebody's document, and pressing Up in the middle of writing one would put "dir" into it. The history belongs to the thing whose lines are commands. Two entry points rather than a flag the caller sets first, so a caller cannot forget which it wanted. And the console is put back the way it was FOUND rather than the way the shell likes it. A program that had asked for key mode and then read a line through the system used to be handed back a console in line mode having asked for nothing of the sort. The status port reports all three things the control port can ask for, in the same order two bits along, so one shift turns what the console IS into what to write to make it that again. Which uncovered a real fault in the console. READING THE STATUS PORT WAS EATING A KEY: in line mode the poll consumed an arrow key and dropped it, so a program that looked and then asked for key mode - exactly what reading a line now does - found the first key it was reaching for already gone. A look must not consume what it cannot report, because the mode can change. It is held now and delivered as soon as something will take it. A blocking read still discards it, and must: that read IS the delivery, and a byte held there would be met again forever. Four recordings gained a program's echo, and cosmosEdit's went from "> : : : : > : : > 1: alpha" to a session you can read. VERIFIED THE SAME WAY AS BEFORE: with only the program side of the echo silenced, all 192 tests pass against the recordings as they were before this commit, so the echo is the whole of what changed. cosmosEditService is the new test and it checks both halves at once. Inside Edit, Left/Delete/Left puts "alpah" right. Up and Down do nothing there - were a program's line walking the shell's history, the next line would come out as the echo command from the top of the file instead of the word. And one press of Up back at the prompt finds the command typed before Edit was started, which is the proof that nothing the editor read went into the history at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |