Having the disk is not the same as knowing how to run it. Which machine — an
Agat-7 or an Agat-9, and with how much RAM? And which key on the keyboard in
front of you sends the byte the program is waiting for? None of that is in a
.dsk.
An .agc file is one program and everything needed to run it: the disk image
(with its patches, and with whatever a program has written to it), the machine,
the settings, and the keyboard. It can carry one more thing when it is asked
to — the machine as it stood, so that it reopens where it was left rather than
booting.
It is JSON: everything a person writes or reads is text in the file, and only the disk image inside it is packed — base64, gzipped when that makes it smaller, which for an Agat disk is by ten times or more. Drop one on the emulator and it runs.
The easiest way to get started is to load a bare image into the emulator and press Save: the container it writes is a text file you can edit.
{
"agc": 1,
"title": "RISE OUT",
"author": "Andrew Maltsev",
"date": "1989",
"url": "https://github.com/amaltsev/agat-web",
"notes": "Carries the original 1989 sound data.",
"machine": { "model": 7, "ram": 64 },
"keys": {
"KeyW": { "code": "^" }
},
"controls": {
"Play": {
"Up Down Left Right": "Move",
"^": "Shoot right"
}
},
"info": "A platform game written for the Agat-7 in 1989 and restored from the author's own tape.",
"hint": "Press РУС at the title screen or the menu comes up in Latin.",
"media": [
{
"name": "rise-out.dsk",
"data": [
"AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2",
"…"
],
"patches": [ { "at": 45312, "hex": "A9 60 85 84" } ]
}
]
}
Only agc is required, and in practice media: everything else has a sensible
default, and a container that carries nothing but an image is a valid one.
The data above is a 140K disk, and a real container carries one as "gz"
instead — the same bytes gzipped before the base64 — because that is ten times
smaller. media describes both, and which one appears is only ever a
question of which is shorter.
| field | |
|---|---|
agc |
format version — 1. Its presence is what identifies the file. |
title |
what the program is called |
author |
who wrote it |
date |
text, not a number: "1989", "circa 1985", "1990-92" |
url |
where it came from, or where it is written up |
notes |
provenance, credits, what a patch does. Ignored by the code. |
These fields are frequently the last place any of this is recorded. Fill them in.
title, author, date and url are drawn on the info card, the last
thing on the page under the controls: the title, then who wrote it and when and
where it came from, with the url a link where it is http/https and printed
plainly where it is anything else. A container that names none of them, and
nothing below either, has no card — which is what a bare image gets.
Two more fields are drawn under that row, and they are the two the card is for:
info, which is what the program is at whatever
length that takes, and hint, the one
thing whoever is about to play has to be told, printed heavier because it is the
line worth acting on. hint inside a keys entry is the same word for one key
on the on-screen board.
notes is the odd one out: it is for the record and nothing reads it. Anything
the reader is meant to see is info or a hint; notes is the file talking to
whoever opens it.
machine| field | |
|---|---|
model |
7 or 9 |
ram |
base RAM in kilobytes: 32, 64 or 128. Agat-7 only — the Agat-9 is always 128K. |
monitor |
the monitor the program was drawn for: "color16" (the default, left unwritten), "color8", "color16inv" or "gray" |
boot |
what starts, when the default is not what this container wants. See media. |
slots |
what this machine has that the model’s stock complement does not. Optional. |
ram is base RAM on the motherboard, not the machine’s total. It is not
cosmetic either: it is the only memory the video controller scans, and it masks
the video mode register’s page field, so software can tell — a disk that expects
64K may fail on 128K.
monitor is which colors the 4-bit codes come out as: the machine puts a bare
code on the RGB connector and the monitor decides. color16 is the common
ВТЦ 202, where the brightness bit brightens; color16inv is the earlier
modification where it darkens; color8 is a monitor without the bit wired, on
which codes 8-F look exactly like 0-7 — and a program whose author drew
it on one mixes the two halves freely, which is what this field is for; gray
is the composite «Видеосигнал» connector. The tables are in
HARDWARE.md.
The stock Agat-7 is 128K in three devices: 64K of base RAM, a 32K ЭмПЗУ in
slot 2 and a 32K ОЗУ expansion in slot 4. The Agat-9 is 128K and two drives.
A container that wants that machine says nothing but model and ram.
machine.slotsKeyed by slot number, 0-7. A slot not named keeps whatever the stock machine
puts there; null empties it.
"machine": {
"model": 7,
"ram": 64,
"slots": {
"4": { "card": "xram", "ram": 128 },
"2": null
}
}
| field | |
|---|---|
card |
"psrom" (Agat-7 ЭмПЗУ), "xram" (Agat-7 ОЗУ expansion), "xram9" (Agat-9 ОЗУ expansion), "fdd140", "fdd840", "mouse-nippel", "mouse-mars", "mouse-mars-rom", "mouse-mm8031", "printer-sm6337" |
ram |
kilobytes, for the memory cards: 16, 32, 48, 64 or 128 |
drives |
2 for a second drive on a controller’s cable. Both controllers select between two; a machine was as good as always fitted with one, which is what a slot that says nothing gets. |
paper |
for a printer, the sheet loaded into it: "A4", "A3" or "fanfold" (15×11″). Absent is whatever the viewer last chose, A4 at first. |
card is required: an entry that gives only a size names no card, and is
ignored.
A mouse is in no stock machine, so a program that wants one has to say
so — and which one, since the three speak different protocols and a
program supports the one it was written for (for example, MouseGraf 4.4
wants mouse-nippel, 1.6 mouse-mars). mouse-mars-rom is the same
«Марсианка» on a printer card that carries its ROM page, which is a
different machine to the program looking for it: Klondike wants that one
and 1.6 will not touch it. The slot is the container’s to
choose; the page puts one in slot 6 on an Agat-7
and slot 4 on an Agat-9, which is what each model leaves free.
printer-sm6337 is the printer card with a СМ6337’s cable on it, for a program
that prints. The page puts it in slot 3 on an Agat-9 and slot 6 on an Agat-7; a
program that drives the card itself may look in one slot only, and the
container says which — examples/word-master.agc puts it in 5, in place of the
stock 840K controller. paper is what the program’s output was laid out for;
saving from the emulator writes the paper in the printer, and leaves A4
unsaid. A machine with a printer opens with the paper shown. What is printed is
not kept in the container. A printer and a parallel mouse in one machine is a machine in
which one program or another takes the printer card for its mouse — see
HARDWARE.md.
keys and controlsTwo blocks, and the difference between them is which side of the keyboard they are indexed by.
| indexed by | answers | |
|---|---|---|
keys |
a host key — KeyW, Space |
what to press, and what that key is for |
controls |
an Agat code — ^, $5E |
what the program reads, and what each code does |
keys puts an Agat code on a physical key of the keyboard in front of you, and
names the keys the program uses even where no remapping is needed. controls
does not touch the keyboard at all: it is the program’s own list of codes,
grouped and captioned, which the page prints under the screen.
Either may appear alone. keys on its own gives the remap and a board winnowed
to the program’s keys; controls on its own gives the panel and a board
winnowed to the codes it names, with nothing remapped. Together they answer both
halves of the same question, which is why #kbd=used draws them from both.
The left-hand sides look alike and are not: "Space" is a legal word in both,
and means the physical space bar in keys and the code $20 in controls.
keys — the keys the program usesThe Agat’s keyboard is not your keyboard. A program that reads ^ is asking
for $5E, which in ЛАТ needs Shift+6 and in РУС is on
X.
"keys": {
"KeyW": { "code": "^", "hint": "Shoot right" },
"KeyA": "←",
"Space": { "hint": "Jump" },
"ArrowUp": null
}
The key on the left is a browser
KeyboardEvent.code
— KeyW, Digit1, ArrowUp, Space — which names a physical key, not a
character, so a remap is the same on any host layout. Every name this emulator
accepts, and what each of them sends unmapped, is in
What each key sends.
The value is either the code to send, or { "code": …, "hint": … }, or — with
no code at all — a declaration that the program uses the key as it already
is. "Space": { "hint": "Jump" } and a bare "ArrowUp": null change nothing
about what those keys send; they say that these are among the program’s keys,
which is what the on-screen board’s All mapped view is drawn from. A
game whose controls need no remapping still has controls, and this is how it
names them.
A code may be written:
| form | example |
|---|---|
| the character itself | "^", "@", "Ю" |
| hex | "$5E", "0x5E" |
| a name | "Up", "Down", "Left", "Right", "Enter", "Esc", "Space", "Tab", "Bksp", "F1", "F2", "F3" |
Characters cover $20–$7F, which is ASCII plus the Agat’s Cyrillic band in
KOI-7 N2 order, so "Ю" and "Ч" work as written. The one trap is $24, which
the Agat draws as ¤ rather than $: write it as "¤" or as "$24".
A remapped key takes that key over completely: it sends its code in both layouts and with or without Shift or Ctrl. A movement key that changed meaning because a modifier was being held would be worse than no remap at all. What the key used to send is unreachable while the container is loaded, so remap keys the program does not otherwise need.
The hint says what the key does. It is the half worth writing: the on-screen
keyboard shows it, so hovering ^ reads W (Shoot right) rather than
leaving someone to work it out.
Naming every key a program uses, remapped or not, is what the winnowed board needs: the machine’s caps, with every one no listed key reaches shrunk to a sliver, so the keys that are left keep the positions the Agat gives them. It is drawn as three areas that collapse on their own — the typewriter, the arrows and the numeric pad — so a program that uses none of the pad is not shown one, and naming a single arrow brings the whole cluster. The board’s own controls (СБР, УПР, РУС/LAT, РЕГ) are not drawn: they are not the program’s keys, and on a phone they were most of the screen.
The menu offers this board only for a container that names keys or controls, and
on a handheld it is what such a container opens with.
node tools/check.js keys <file.agc> draws it in a terminal.
controls — what the program reads, and what for"controls": {
"Play": {
"Up Down Left Right": "Движение",
"Space": "Стоп",
"^": "Выстрел вправо"
},
"Cheats": { "K": "Самоубийство", "К": "Конец игры" }
}
Groups in the order the file lists them, rows in the order the group lists them.
A row’s key is one or more codes separated by spaces, written any of the ways
above — so the arrow cluster is one line
rather than four, and "Space Enter" is one line for a program that takes
either. The value is what that row does; true means a control worth naming
with nothing to add.
Codes, never combinations. The Agat keyboard is an encoder that puts one byte
in $C000. РЕГ adds $20 across the letter block — that is why the caps are
dual-legend — so what a person calls РЕГ+К is the single code $6B, written
"К" or "$6B". УПР collapses the same way into $81–$9F. There is nothing
a + could mean here, and it is not accepted.
Three things read this block:
Q is $51
whatever is switched on — and the board beside it answers the other half, which
host key reaches that code right now. The only host key on the panel is a
container remap, ^ (W), because a remap holds in every plane and so is the
only one that does not move under ЛАТ/РУС.keys block reaches.#kbd=used%3ACheats. Tapping a group on the panel picks the same thing with a
finger, and tapping the one already showing goes back to all of them.Two traps worth knowing:
"1"
jumps to the front of its group. Write it "$31" — which is what
edit-agc.html writes for you, a group named with a digit
being the one thing it refuses.K and К are the unshifted and
shifted halves of a single Agat cap, so a container naming both gets one key
on the board. It is drawn with both halves underlined and names both in its
tooltip, and the board grows a РЕГ cap — the one control it otherwise
never draws — because without a register the shifted half could not be
reached by touch at all. Tap РЕГ, then the key. It is a one-shot, as it has to
be with one finger, and it appears only when some cap needs it.node tools/check.js keys <file.agc> prints the panel and the board together,
and --group=NAME cuts them the way the menu does.
info — what the program is"info": "A platform game written for the Agat-7 in 1989 and restored from the author's own tape."
The description, printed under the author-date-url row. As long as it needs to
be: what the program does, who it was for, which release this is, what a patch
in it changed — the things a title has no room for and notes used to have to
swallow. A container that carries only this gets a card with only this on it.
Same plain text rule as the hint below, and the same reason: whitespace
collapses to one paragraph, and markup is printed rather than obeyed. One thing
in it is recognized: a bare http/https address becomes a link, in the hint
as well as here, because a container that says where a program is written up
says it in the middle of a sentence as often as in its url. The text is left
as it was typed, scheme and all — in prose the address is part of the sentence —
and the full stop that ends the sentence stays out of the link.
info and notes are easy to mix up, and the split is who is reading. info
is shown, so it is written for whoever opens the program; notes is not, so it
is written for whoever opens the file.
hint — the line the player is shown"hint": "Press РУС at the title screen or the menu comes up in Latin."
One sentence or two, printed at the foot of the info card, and printed heavier than the rest of it: it is the line worth acting on rather than reading. It is for the thing no list of codes can say: which layout the program comes up in, that the first disk is the one to boot, that the pause key is also the quit key. A container with a hint and nothing else still gets the line — the card is drawn for any one of the six things on it.
Plain text. No paragraph breaks, no Markdown, no HTML: the page prints it as
text, so a <b> shows up as <b>. Whitespace collapses, so a hint wrapped
across lines in the file is one line on the screen and is written back as one.
A bare http/https address in it becomes a link, as it does in
info, and nothing else is interpreted.
It is the same word as a key’s hint, and the same rule: a hint is shown. This
one is the container’s, and keys.<key>.hint is one key’s. notes is the other
kind — the record, which nothing reads. A container can carry all three.
node tools/check.js keys <file.agc> prints the card under the panel, ending in
the hint, where the page puts both.
mediaA list, loaded in order. Disks go to whichever drive can read them; a .fil is
poked straight into memory.
| field | |
|---|---|
name |
the original filename. The format is detected by size, not by this — Agat images in the wild are routinely misnamed. |
in |
which drive it goes in. Optional; see below. |
writable |
true and a program may write to this disk. Absent is locked, which is what every disk arrives as. |
data |
base64, as an array of lines |
gz |
the same bytes gzipped, then base64. data or gz, never both. |
patches |
changes to apply after decoding, in order; hex, base64 or gzipped |
Lines are 76 characters, which is 57 bytes and a whole number of base64 groups, so each line stands on its own. A single long string is accepted on reading; hand-wrapped lines of any width are too.
data or gz is a size decision, and the writer makes it: gzip is used
when it saves at least a tenth, and data otherwise.
Anything the emulator takes, a container carries: .aim, .dsk and
.nib at 140K and 840K, and .fil programs. A container inside a
container is refused. Each image goes into the drive its size implies,
so a container can fill both drives. Several .fil programs are loaded
in the order they are listed (and the last loaded runs).
A container may carry more disks than the machine has drives. The drives are filled from the list, in order — the first 140K image to the 140K drive, the next to its second drive if one is fitted — and what is left over is carried without being in a drive. It is still a disk of this session: it can be put in a drive from the page, and it is saved with the rest. That is what makes an editor on one disk and the work on another one container.
in says where a medium goes, when the order is not the answer:
"fdd140", "fdd840" |
that controller’s first drive |
"fdd140:2" |
its second, on a controller drives fitted one to |
"slot:3", "slot:3:2" |
by slot, for a machine with two of the same card |
"none" |
carried, and in no drive |
| absent | the fill order above |
A medium naming a drive this machine has not got is read as saying nothing, and
takes its turn with the rest. Save writes the fewest of these it can: the
load is replayed against the list, and only a disk the order would put somewhere
else is told where it is — so a container of one disk carries no in at all.
Everything listed is loaded first, in order; then one thing starts. Without a
boot that is the first disk listed, whichever drive it went into, and a
container of nothing but .fil programs is left running the last of them.
machine.boot overrides it:
"fdd140", "fdd840" |
that controller, wherever this model puts it |
"slot:N" |
that slot, for a machine with two of the same card |
"none" |
nothing — a .fil runs and the disk is only mounted |
"monitor" |
the machine’s own scan picks, as the hardware does |
"auto", or absent |
the default above |
"monitor" is the real machine: both models enter the 840K controller in slot 5
whether or not it holds a disk, so a container that names a 140K disk and asks
for "monitor" sits there waiting, exactly as an Agat would. Everything else
is ПР#n into the drive named.
A boot naming a card this machine was not built with cannot be honored — there
is no slot to enter — so the default is taken and the page says why. A slot that
is empty but real is entered as asked.
The same rules cover what is opened by hand: a drop or a Load of several
files is read as a container whose media are those files in that order.
patches"patches": [ { "at": 45312, "hex": "A9 60 85 84" },
{ "at": 46080, "data": ["…", "…"] },
{ "at": 49152, "gz": ["…", "…"] } ]
at is a byte offset into the decoded payload, and the bytes to write are one
of hex — whitespace and commas allowed, so they can be grouped the way they
mean something — data, base64 in the same wrapped form as a payload, or gz,
gzipped and then base64. A reader takes all three. A record that gives two at
once is an error, not a preference to resolve.
The writer picks by size, and it is the same rule the payload gets: up to 32 bytes hex, above that whichever of base64 and gzip is smaller.
Any other key on a patch record is left alone — a container is hand-edited, and
a "why" beside the bytes should survive being loaded and saved.
That key is also what decides whether a record survives a save. A save does not add its difference to the list it found; it keeps every annotated record verbatim, throws the plain ones away, and works the difference out again against the finished image. So a change and its undo — rename a file on a disk and rename it back — leave nothing behind rather than two records at one address that cancel out, two writes to the same bytes come back as one, and saving twice gives the same file.
What it costs is that two plain records close enough together come back as one,
diff’s eight-byte gap being what decides. A hand-written patch that wants to
stay a patch of its own needs a word on it saying what it is for — which is
worth writing down anyway, and is the only thing that tells a reader the bytes
were somebody’s decision rather than a disk’s arithmetic.
The payload stays the image exactly as it was found, and changes live here. That is the whole point of the split: a container carries a pristine copy of what it came from, the change is legible to anyone reading the file, and saving a container that was loaded writes back what it carried rather than the patched result.
What a program writes to a disk is saved the same way. A 140K drive that has been unlocked writes to the nibble stream in memory; saving reads each written track back into the 16 sectors it was built from and records the difference here. A game that keeps a high score costs a patch of a few hundred bytes — one base64 block, by the size rule above — rather than a second copy of the disk.
A track that will not read back as sectors — a disk formatted some other way, a
write caught half done — has no sector image for a patch to be the difference
from. Then the whole nibble stream is saved instead, as a .nib payload with no
patches. It is a bigger file, but not a lossy one, and it reloads without any of
this having to know: media are identified by size.
state — the machine as it stoodEverything above says what a program is. state says where a particular
person had got to in it: the RAM, the CPU’s registers, the raster counter, the
banking registers on every card, where each drive left its head. A container
that carries one resumes when it is opened, instead of booting.
"state": {
"version": 1,
"cycles": 91234567,
"cpu": { "a": 0, "x": 3, "y": 255, "s": 248, "p": 52, "pc": 2051 },
"machine": {
"model": 7, "ramSize": 65536,
"mode": 6, "rasterLine": 143, "nextLine": 91234700.5, "irqRaw": true,
"psromMode": 1, "kbdLatch": 0, "cyrillic": false, "speaker": 0,
"palette": 0, "mem7": 0,
"ram": { "gz": ["…"] }
},
"slots": {
"2": { "state": 128, "ram": { "gz": ["…"] }, "card": "psrom", "size": 32768 },
"3": { "drv": 0, "motor": 1, "time": 91234000, "seed": 623456789,
"heads": [{ "phase": 34, "track": 17, "index": 3312, "rotated": 1 },
{ "phase": 20, "track": 10, "index": 0, "rotated": 0 }],
"card": "fdd140", "locked": false }
}
}
| field | |
|---|---|
version |
the snapshot’s own format version — 1. Not agc: see below. |
cycles |
the master clock, in CPU cycles since the machine was switched on. Every other timestamp in the block is an absolute value on this one scale. |
cpu |
a x y s p pc, and halted/irqLine/irqPending/nmiEdge where they are set |
machine |
the model and base RAM size it is for, every register reset() touches, and ram — base RAM, as a payload is written |
slots |
one entry per fitted card, keyed by slot number |
A slot entry is whatever that card holds, plus the fields the slot rather than
the card contributes: card, size and drives, which say what has to be in
the slot for the snapshot to mean anything, and locked, a drive each — the
disk’s write lock, which is the person’s choice rather than the program’s and
would otherwise come back on. (A snapshot written before there were two drives
carries one flag rather than a list, and it is the first drive’s.)
The disk is not in here. What a program wrote to it is carried by the
medium’s patches, as it is in any container, so a restored drive finds the
disk it was reading and a container that is saved twice is the same file both
times.
Neither is anything host-side: the speaker’s queue, the pointer, the wall clock.
The cone’s position is the machine’s — a program can read it at $C030 — and
that is saved; the queue of edges waiting to be played is the page’s and is not.
A state that does not fit the machine is refused, not forced. It names the
model, the base RAM size and every card, and if the machine that got built is
not that machine the container boots as it would have without one and the page
says why. That is what makes #agc=game.agc&model=9 do something sensible: the
address asks for a machine the snapshot is not about, and the program starts
from the beginning on it.
Saving one is a checkbox on Save’s file, and it is off unless the container being saved arrived with a state already. A save into the browser always carries one — see Making one.
recordings — sessions played backA state says where somebody had got to. A recording says what they did next:
the same snapshot, and every input since, each stamped with the cycle it landed
on. Playing one back into the snapshot runs the same program the same way,
because the machine is a function of its state and its inputs and nothing in it
reads a clock.
"recordings": [ {
"version": 1,
"name": "level 1",
"wall": 1756530000000,
"edited": 1756617000000,
"autoplay": true,
"cycles": 91234567,
"ended": 100234567,
"stopped": "user",
"state": { "…": "a state block, exactly as above" },
"events": [ [370000, "k", 160], [1900000, "k", 149], [230000, "m", 3, -1] ]
} ]
| field | |
|---|---|
version |
the recording’s own format version — 1, and not agc, for the reason state’s is |
name |
what to call it where more than one is offered |
wall |
when it was recorded, in milliseconds since the epoch — for the person, never for the machine |
edited |
when it was last added to, where it has been. A take can be picked up again from where it ends, or from the middle of a replay, and then it is one recording made in two sittings |
autoplay |
play it as the container opens, on the keyboard the take’s own keys are on. A demo container says so here; absent means no |
cycles |
the master clock where it begins, the same scale state uses |
ended |
and where it stops |
stopped |
user, write or machine — why it stops, which is worth saying for a take that ends mid-sentence |
state |
the machine it begins on, as state above |
events |
the inputs, in order |
An event is [dcycles, kind, …] — the cycles since the one before it, relative
because a recording is one thing after another and absolute stamps this long
are mostly the same leading digits. They are written one to a line, whatever the
indentation around them: a take of a few thousand inputs is unreadable and four
times the size with each event spread over four lines. The kinds are what the machine can see:
| kind | |
|---|---|
k code |
the byte in the keyboard latch, $C000 |
l 0|1 |
ЛАТ / РУС, which software reads at $C063 |
m ix iy |
whole mouse counts, the host’s pixels already spent |
b mask |
the two mouse buttons, bit 0 A and bit 1 B |
x wall |
not an input at all: the take was picked up again here, and this is the moment by the clock on the wall. Nothing reads it yet; it is what a file has to say about how it was made |
Nothing about the host is in one: no scancode, no pixel, no millisecond. A reader that meets a kind it does not know should skip that event rather than refuse the recording.
Picking a take up again keeps its snapshot. What is dropped is every event
past the cycle the machine is standing on, and what follows is recorded over it
— sound because the machine got to that cycle by playing those same events back.
The join is the x event, and edited says when.
A recording assumes the disk it was made on. The media are not in the snapshot, so a take stops at the first write — and a disk written after one was made, then saved, leaves the take starting from a disk it never saw. It still plays; it may not play the same. A list, though the page makes and plays one at a time: a container is one program, and the disk in it is 600K that several takes should not each carry a copy of.
edit-agc.html is the page for the container itself. A
.dsk, .nib, .aim or .fil dropped on it becomes a container carrying that
image, with every field of this document as a control; an .agc dropped on it
opens for editing. It writes through the same src/agc.js the emulator’s Save
AGC and tools/agc.js write through, so a container made any of the three
ways is the same file — and it keeps the field order of whatever it opened, so
a hand-arranged container stays arranged.
Save writes a container from the machine as it stands: what was opened —
what is in the drives, and any .fil poked into memory — the model and RAM, the
live remap, and anything a program has written to an unlocked disk. What was
opened and not what has been open: opening empties the drives first, so the
container is about one program and the several files of one gesture rather than
the session. It asks one thing, and only because the answer is a
different document either way: whether to carry the machine
state — where the program had got to — along
with the program. A container without one is something to hand to somebody; one
with a state is where a particular person was. The box starts ticked for a
container that already came with a state and clear for one that did not,
including every bare image.
A recording made in this session travels with the container and is not asked about: unlike a snapshot it is not somebody left in the middle of somebody else’s program, it exists because it was deliberately made.
Nothing else is asked, including about compression, which is decided per payload, per patch and per snapshot by whether it pays.
Save in browser writes the same container to IndexedDB instead of to a file, where the page’s Load lists it and opens it again. It asks nothing: a save kept here is where somebody was, so the machine always goes in with it. What is stored is the container text itself, so a save is the file it would have been and reopening one is reopening a container.
A container that was loaded from a file keeps its own title, and its filename
with a -yyyymmdd-hhmmss stamp on it, so game.agc saves as
game-20260825-143012.agc and sits beside the file it was made from instead of
over it. An existing stamp is replaced rather than added to. One made from a
bare image takes the image’s name for both, unstamped — that save is a first
one — so game.dsk saves as game.agc titled game.dsk. Rename either, and
add an author, a date and the keys with tools/agc.js edit or a text editor.
tools/agc.js is the container as a command: it makes one, says what one holds,
and changes one that exists.
node tools/agc.js make game.dsk \
--title="…" --author="…" --date=1989 --url=https://… \
--model=7 --ram=64 --info="A platform game of 1989." \
--hint="Press РУС at the title screen." \
--key="KeyW:^:Shoot right" > game.agc
--key is CODE:VALUE:HINT, split on the first two colons, and may be repeated.
--patch=AT:HEX states a patch directly; --diff=<modified image> works the
patches out by comparing a changed copy against the original, which is how a
patch is usually arrived at. --model, --ram, --monitor, --boot and
--slot=N:CARD[:RAM[:DRIVES]] are the machine; --in and --writable are the
drive a medium goes in and whether a program may write to it.
Every one of those flags means the same thing to edit, which changes what an
existing container says and leaves its media alone — an empty value clears a
field, and --no-key=KEY drops a key:
node tools/agc.js info game.agc # what it holds
node tools/agc.js edit game.agc --date="1990-92" --url=
node tools/agc.js get game.agc 0 --out=/tmp # the disk back out as a file
node tools/agc.js add game.agc side-b.dsk --in=fdd140:2
node tools/agc.js rm game.agc 'side-b*'
node tools/agc.js merge game.agc # patches into the image
A medium is named by its position or by its name, where * and ? glob. add
takes a container as well as an image, and then takes its media, patches and
drives and nothing else. get writes the image the machine runs — the payload
with the patches applied — which is what merge writes back into the container
itself, for a disk that has been written to and is not going to be written back.
An annotated patch is somebody’s writing about a change, so merge refuses to
lose one until --force says to.
--plain writes every payload and patch as base64 whatever it costs, for a
container meant to be hand-edited or read in a diff; --gz compresses even
where the saving is slight. Left alone, the size rule decides.
A field this reader does not know is dropped rather than carried through, so
a hand-written key on the container itself does not survive an edit — patch
records are the exception, and keep their annotations.
Drop it on the page, or use Load, or name it in the address:
index.html#agc=examples/rise-out.agc
index.html#agc=https://example.org/games/tetris.agc
The address form fetches the file, so it needs a served page — fetch is
blocked on file://, where Load is the way in. What it names is a path
beside the page or an https:// URL to a container hosted anywhere that sends
Access-Control-Allow-Origin: * header: a container is a program plus the machine it
runs on, so a link to one somebody else hosts is a link to something that runs.
The address’s other keys go
into the machine the container builds rather than on top of it, so
#agc=…&model=9 tries the program on the other machine without editing the
file, and the other machine is the one it boots on. Boots, and does not
resume: a container carrying a state put on
a machine the snapshot is not about says so and starts the program from the
beginning. Each of them is a
difference: a key appears only where the machine and the container disagree, so
a container running as it asks to leaves #agc=… and nothing else, and the
address follows the container if the container is later changed.
Every command-line tool takes a container wherever it takes an image, and runs it on the machine the container names:
node tools/check.js sniff game.agc # what it says it is
node tools/check.js boot game.agc # boot it and report where it got to
node tools/shot.js game.agc # boot it and write a PNG
Every name below is accepted on the left of keys; anything else is ignored,
and the status line says which. The columns are what the key sends when it is
not remapped, which is also what a key declared as-is goes on sending.
Each cell gives the glyph the Agat draws and the code itself, written the way
keys and controls take it. The byte in $C000 always has bit 7 set, so
$40 and $C0 are the same key; the tables give the 7-bit form wherever there
is a glyph to go with it.
A letter’s two halves are one byte in two character sets: РЕГ adds exactly $20
across the block, which moves ASCII @A-Z[\]^_ into the Agat’s Cyrillic band in
KOI-7 N2 order. That is why both legends fit on one cap, and why Ч is РУС
X and nothing at all in ЛАТ.
Codes with no glyph are given in hex, written the way keys takes them. — is
a key the table maps to nothing: Insert, Delete and F4-F12 are free to
take over, since nothing is lost. Backspace and ArrowLeft both send $88,
which is the machine having one ←.
Letters — these follow ЛАТ/РУС and РЕГ
code |
ЛАТ | ЛАТ+РЕГ | РУС | РУС+РЕГ |
|---|---|---|---|---|
KeyQ |
Q $51 | Я $71 | J $4A | Й $6A |
KeyW |
W $57 | В $77 | C $43 | Ц $63 |
KeyE |
E $45 | Е $65 | U $55 | У $75 |
KeyR |
R $52 | Р $72 | K $4B | К $6B |
KeyT |
T $54 | Т $74 | E $45 | Е $65 |
KeyY |
Y $59 | Ы $79 | N $4E | Н $6E |
KeyU |
U $55 | У $75 | G $47 | Г $67 |
KeyI |
I $49 | И $69 | [ $5B | Ш $7B |
KeyO |
O $4F | О $6F | ] $5D | Щ $7D |
KeyP |
P $50 | П $70 | Z $5A | З $7A |
KeyA |
A $41 | А $61 | F $46 | Ф $66 |
KeyS |
S $53 | С $73 | Y $59 | Ы $79 |
KeyD |
D $44 | Д $64 | W $57 | В $77 |
KeyF |
F $46 | Ф $66 | A $41 | А $61 |
KeyG |
G $47 | Г $67 | P $50 | П $70 |
KeyH |
H $48 | Х $68 | R $52 | Р $72 |
KeyJ |
J $4A | Й $6A | O $4F | О $6F |
KeyK |
K $4B | К $6B | L $4C | Л $6C |
KeyL |
L $4C | Л $6C | D $44 | Д $64 |
KeyZ |
Z $5A | З $7A | Q $51 | Я $71 |
KeyX |
X $58 | Ь $78 | ^ $5E | Ч $7E |
KeyC |
C $43 | Ц $63 | S $53 | С $73 |
KeyV |
V $56 | Ж $76 | M $4D | М $6D |
KeyB |
B $42 | Б $62 | I $49 | И $69 |
KeyN |
N $4E | Н $6E | T $54 | Т $74 |
KeyM |
M $4D | М $6D | X $58 | Ь $78 |
Digits and punctuation — these follow ЛАТ/РУС and РЕГ
code |
ЛАТ | ЛАТ+РЕГ | РУС | РУС+РЕГ |
|---|---|---|---|---|
Digit1 |
1 $31 | ! $21 | 1 $31 | ! $21 |
Digit2 |
2 $32 | @ $40 | 2 $32 | ” $22 |
Digit3 |
3 $33 | # $23 | 3 $33 | # $23 |
Digit4 |
4 $34 | ¤ $24 | 4 $34 | ; $3B |
Digit5 |
5 $35 | % $25 | 5 $35 | % $25 |
Digit6 |
6 $36 | ^ $5E | 6 $36 | : $3A |
Digit7 |
7 $37 | & $26 | 7 $37 | ? $3F |
Digit8 |
8 $38 | * $2A | 8 $38 | * $2A |
Digit9 |
9 $39 | ( $28 | 9 $39 | ( $28 |
Digit0 |
0 $30 | ) $29 | 0 $30 | ) $29 |
Minus |
- $2D | _ $5F | - $2D | - $2D |
Equal |
= $3D | + $2B | = $3D | + $2B |
BracketLeft |
[ $5B | Ш $7B | H $48 | Х $68 |
BracketRight |
] $5D | Щ $7D | _ $5F | Ъ $7F |
Semicolon |
; $3B | : $3A | V $56 | Ж $76 |
Quote |
’ $27 | ” $22 | \ $5C | Э $7C |
Backquote |
@ $40 | ^ $5E | @ $40 | ^ $5E |
Backslash |
\ $5C | Э $7C | \ $5C | Э $7C |
Comma |
, $2C | < $3C | B $42 | Б $62 |
Period |
. $2E | > $3E | @ $40 | Ю $60 |
Slash |
/ $2F | ? $3F | . $2E | , $2C |
Editing — one code, whatever the layout
code |
sends | code |
sends |
|---|---|---|---|
Escape |
Esc $9B | Enter |
↵ $8D |
Backspace |
← $88 | Space |
space $20 |
Tab |
Tab $89 |
Arrows and the nav cluster — one code, whatever the layout
code |
sends | code |
sends |
|---|---|---|---|
ArrowUp |
↑ $99 | End |
$8A |
ArrowLeft |
← $88 | PageUp |
↑ $99 |
ArrowRight |
→ $95 | PageDown |
↓ $9A |
ArrowDown |
↓ $9A | Insert |
— |
Home |
$8B | Delete |
— |
Function keys — one code, whatever the layout
code |
sends | code |
sends |
|---|---|---|---|
F1 |
F1 $84 | F7 |
— |
F2 |
F2 $85 | F8 |
— |
F3 |
F3 $86 | F9 |
— |
F4 |
— | F10 |
— |
F5 |
— | F11 |
— |
F6 |
— | F12 |
— |
The numeric pad — one code, whatever the layout
code |
sends | code |
sends |
|---|---|---|---|
NumpadMultiply |
* $2A | NumpadAdd |
+ $2B |
Numpad7 |
$90 | Numpad1 |
$9D |
Numpad8 |
$91 | Numpad2 |
$9E |
Numpad9 |
$92 | Numpad3 |
$9F |
NumpadSubtract |
- $2D | Numpad0 |
$81 |
Numpad4 |
$93 | NumpadDecimal |
$82 |
Numpad5 |
$94 | NumpadEnter |
$83 |
Numpad6 |
$9C | NumpadDivide |
/ $2F |
agc. A reader should refuse a file whose version it does not
know rather than guess at it. There is only the one so far: compression
arrived without moving it, because which encoding a record uses is written in
the record and needs no number to tell it apart.agc key, not by the extension.ram is in kilobytes and date is a string. Both are the kind of thing that
is easy to guess wrong in a second implementation.info and hint are shown and notes is not. Keep all three, and do not let
one be read as another: they are the same kind of prose written for different
readers, and a reader that folds them together loses which is which for good.data or gz, and a patch one of hex, data or gz —
never two. Read any of them; refuse a record that gives more than one.state has a version of its own, and it is not agc. A container carrying a
snapshot a reader does not understand is still a container and should still
boot; only the snapshot is refused. That is also why adding state did not
move agc: a reader that has never heard of it ignores it and boots, which is
a correct outcome rather than a broken one.data would still produce files this one reads.The reference implementation is src/agc.js — about 600 lines,
no dependencies, and the same file reads and writes. respec is the join
between the two directions — a container as parse returned it, back as build
takes it — and it is worth having rather than open-coding, because it is the one
place a field that nothing edits gets carried across. Both directions are
asynchronous there, because gzip in a browser is a stream; everything between
them works in plain bytes, and a patch in memory is { at, bytes }. state
passes through it packed and untouched: what is inside a snapshot belongs to
src/state.js, which is also where the fitting check lives.