Skip to content

x.1sav.dirent

Owner: these specifications, under a reserved name · Applies to: a card’s or save’s part · Status: normative schema, optional to carry

When a card’s directory dates a save and when it says the console last wrote it, on a scale a consumer can read without knowing how that card counts time, and the zone those times were kept in where a producer knows it. The entry itself is carried verbatim in dirent, which is enough to write the save back onto a card and useless for anything else. The name sits in the x tree because the value belongs to no single producer.

; ============================================================================
; x.1sav.dirent - what a card's directory records about one save
;
; An extension key for the Universal Saves Format, defined by these
; specifications under a name reserved in the Vendor Names registry because the
; value belongs to no single producer.
;
; The root here is the value carried under the key, not a bundle. A bundle
; validates against the format's own schema whatever this holds, since an
; extension value is `any` there; this file is what makes the shape checkable.
; ============================================================================
; Written as a choice rather than two optional keys so that an empty map is not
; a valid value: a producer with neither time omits the key instead.
dirent = {
0 => entry-time, ; created_at - when the directory dates the entry
? 1 => entry-time, ; modified_at - when the console last wrote it
} / {
1 => entry-time,
}
; A reading, and the zone it was taken in where a producer knows it. An array
; rather than a second key on the map, so the offset binds to the reading it
; qualifies: a save created in winter and last written in summer was kept in two
; offsets, and one field beside the pair could not say so. It is also the shape
; `x.1sav.icon` already uses for a `frame`, a value and the qualifier it may
; turn out not to have, and it leaves an offset with no reading beside it
; unwritable rather than merely invalid.
entry-time = [
entry-seconds,
? utc-offset,
]
; The wall clock the console showed, as whole seconds on the Unix scale, reached
; by reading that wall clock as though it were UTC. Tag 1 for the reason
; `created_at` gives in the format spec: it admits a float, and allowing one
; would give a single reading two encodings. Directories count from wherever
; suits them, a GameCube entry from 2000-01-01 and a Saturn entry in minutes
; from 1980-01-01, and converting that once is the work this key exists to do.
;
; What it is not is an instant. Most consoles here never record the zone their
; clock was set to, so the seconds alone fix a wall clock rather than a point in
; time, and the offset beside them is what closes that gap where a producer can.
entry-seconds = #6.1(int)
; Seconds east of UTC, so the instant is the reading minus this. Present only
; where a producer knows the zone the console's clock was set to, which is what
; lets the first element mean the same thing whether a second follows it or not:
; a consumer that reads only the head of the array gets the wall clock the card
; recorded, which is never wrong, only less than the whole answer.
;
; The range is the span real zones occupy, -12:00 through +14:00.
utc-offset = -43200..50400

created_at is the time the directory dates the entry from, and modified_at the time it records for the last write. A format carries whichever it keeps: a GameCube entry has a modify time and no creation time, a VMU entry the reverse, a PS2 entry both. A producer holding neither omits the key, which is why the schema is a choice rather than two optional keys.

Both are the wall clock the console showed, put on the Unix scale by reading that wall clock as though it were UTC. They are deliberately not instants. A GameCube sets its clock in the IPL menu with no notion of a zone, and a VMU is the same, so the seconds a directory holds fix a date and a time of day and say nothing about where on Earth that was. Calling such a reading UTC would be a guess dressed as a fact, and one a consumer could not see through.

The offset is what closes the gap, in seconds east of UTC, so the instant is the reading minus the offset. It follows a reading only where a producer actually knows the zone the console’s clock was set to: a PS2 keeps its directory times in a fixed +9 whatever the console’s region, and a producer that read the card as part of a device whose source records where the read happened knows it another way. A producer that does not know writes the one-element form, which is the common case and not a degraded one.

The first element means the same thing whether a second follows it or not, which is the point of qualifying the reading rather than replacing it. A consumer that reads only the head of the array gets the wall clock the card recorded, in every case, and is never wrong by hours without being told. One that reads the tail gets a true instant wherever a producer could supply one.

The zone rides on each reading rather than on the entry because one entry can hold two of them. A save created in winter and last written in summer was kept at two offsets on the same console, and a single field beside the pair could not say so. It is the shape x.1sav.icon already uses for a frame, a value and the qualifier it may turn out not to have, and it leaves an offset with no reading beside it unwritable rather than merely invalid.

Four of the seven card formats date an entry, and PS1, N64 and Neo Geo carry no time. Saturn’s date is in its entry like the others’, even though that entry sits at the head of the save’s first block rather than in a directory region.

The part that stands for one entry in a directory, which is two places and no others: a card’s bundle part holding a save, and a save’s own part where the save is a directory, as a PS2 save’s files are. These times are the directory’s record and do not survive the save being sliced out, which is why they sit beside the raw dirent bytes rather than in the save’s own header.

It does not go on a device’s part or a collection’s. Those parts are components and entries, which sit in a socket and in a file rather than in a directory, and neither has an entry to normalize.

What does survive is scoped the other way. x.1sav.label and x.1sav.icon sit on the nested bundle’s header, and x.1sav.rtc goes further and sits only on a save’s header, for the same reason in reverse.

  • What the save is called, and what it looks like. x.1sav.label and x.1sav.icon.
  • Block counts. Derivable from the payload and the format’s block size.
  • The name the card’s directory holds. path, which a consumer matches on.
  • Permission bits, copy protection, a VMU’s mini-game type byte. Format-specific, so they belong under a key named for the card format it decomposes. A producer that wants a consumer to rebuild an entry carries both keys.

Only a format whose entry holds something a writer cannot get elsewhere gets one, and two of the seven do not.

FormatKeyWhy
gc-mcx.1sav.dirent.gc-mcComment and image addresses, icon format, permissions.
ps2-mcnot yet writtenMode bits, attributes and two timestamps per entry.
vmunot yet writtenThe mini-game type byte, copy protection, a header offset.
n64-cpaknot yet writtenA company code and the note’s own character encoding.
neogeo-mcneverIts 4 bytes are an NGH number and a sub-number, which are path, and a FAT index, which a writer regenerates.
saturn-bupnot yet writtenThe comment’s language byte, which nothing else carries.
ps1-mcneverIts frame holds a filename, a state byte and a block link: path, and two things a writer regenerates.

Only when the producer read the entry. A producer that would be guessing omits the key, and MUST NOT substitute the time it performed the dump, which is created_at in the bundle header.

The offset is a separate decision from the reading it follows. Not knowing the zone is a reason to write the one-element form and no reason at all to drop the reading, which is what qualifying it separately buys: before, a producer reading a GameCube entry had a modify time it could not honestly convert, and the only conforming move was to drop a field the card really does hold. A producer MUST NOT fill the offset with the zone the dump happened in, which is a fact about the reader rather than about the console, nor with a default of zero, which claims UTC and means it.