Varvara is a hardware abstraction layer for Uxn.
Varvara is a specification for devices communicating with the Uxn machine intended to run little audio and visual programs..
- Devices are external systems connected to Uxn, such as the screen, the mouse and the keyboard. Each device has 16 bytes of addressable memory in the device page, which are called ports.
- Ports are addresses in the device page, some ports hold either the high or low bytes of a short, and will be presented below as a single port indicated like vector*.
- Vectors are ports holding an address in memory to evaluate when a device event is triggered, such as when the mouse is moved, or a key is pressed. Vector ports will be presented below with a colored background.
- Banks are additional memory outside of addressable range accessible via the System/expansion port. Emulators are recommended to support 16 banks when possible, each bank contains 65536 bytes.
| Varvara | |||
|---|---|---|---|
| 00 | system | 80 | controller |
| 10 | console | 90 | mouse |
| 20 | screen | a0 | file |
| 30 | audio | b0 | |
| 40 | c0 | datetime | |
| 50 | d0 | Reserved | |
| 60 | e0 | ||
| 70 | f0 | ||
The two reserved devices can be used for implementation specific features that do not need to be part of the specs, or other Uxn/Varvara instances.
System Device
| system | 00 | Unused* | 08 | red* |
|---|---|---|---|---|
| 01 | 09 | |||
| 02 | expansion* | 0a | green* | |
| 03 | 0b | |||
| 04 | wst | 0c | blue* | |
| 05 | rst | 0d | ||
| 06 | metadata* | 0e | debug | |
| 07 | 0f | state |
|00 @System/vector $2 &expansion $2 &wst $1 &rst $1 &metadata $2 &r $2 &g $2 &b $2 &debug $1 &state $1
The System/expansion* port expects an address to an operation, it allows basic memory management for roms that must keep information outside of the addressable range, or cache the content of a file in order to seek through the data.
| name | operation | fields | |||||
|---|---|---|---|---|---|---|---|
| fill | 00 | length* | bank* | addr* | value | ||
| Fill a length of memory with a value, starting at address. | |||||||
| cpyl | 01 | length* | src bank* | src addr* | dst bank* | dst addr* | |
| Copy memory starting by the first byte. | |||||||
| cpyr | 02 | length* | src bank* | src addr* | dst bank* | dst addr* | |
| Copy memory starting by the last byte. | |||||||
@on-reset ( -> ) ;cmd .System/expansion DEO2 ;dst print-str BRK @cmd [ 01 000b 0000 =src 0000 =dst ] @src "Hello 20 "World $1 @dst $c
Reading, and writing, to the System/wst and System/rst ports will get, or set, a byte value as the working and return stack indexes. The System/metadata* port notifies the emulator that metadata about the rom is present at the address specified. The emulator can choose to utilize this information or to ignore it.
@on-reset ( -> ) ;meta .System/metadata DEO2 BRK @meta 00 "Nasu 0a "A 20 "Sprite 20 "Editor 0a "By 20 "Hundred 20 "Rabbits 0a "Jan 20 "8, 20 "2023 $2
| color0 | color1 | color2 | color3 |
|---|---|---|---|
#fff |
#000 |
#7db |
#f62 |
This device is holding 3 shorts to be used for application customization, for simplicity we call them the System/red*, System/green* and System/blue* shorts. These colors are typically used by the screen device to form four application colors.
@on-reset ( -> )
set-theme
BRK
@set-theme ( -- )
#f07f .System/r DEO2
#f0d6 .System/g DEO2
#f0b2 .System/b DEO2
JMP2r
Sending a non-null byte to the System/debug port will print the content of the stacks or pause the evaluation if the emulator includes a step-debugger. The recommended output for the #01 debug value, with a working stack containing 3 items, is as follow:
WST 00 00 00 00 00|12 34 56 < RST 00 00 00 00 00 00 00 00|<
If at the end of the current vector, the value in System/state is not zero, the application will stop
listening to new events and terminate. On systems that can handle exit
codes, the error code is the 0x7f portion of the byte. So,
0x01 terminates the program with an error, and 0x80
terminates the program succesfully.
@on-reset ( -> ) ( exit code 0, success ) #80 .System/state DEO BRK
Console Device
| console | 10 | vector* | 18 | write |
|---|---|---|---|---|
| 11 | 19 | error | ||
| 12 | read | 1a | -- | |
| 13 | -- | 1b | -- | |
| 14 | -- | 1c | addr* | |
| 15 | -- | 1d | ||
| 16 | -- | 1e | mode | |
| 17 | type | 1f | exec |
|10 @Console/vector $2 &read $5 &type $1 &write $1 &error $1
The Console/vector* is evaluated when a byte is received. The Console/type port holds one of 5 known types: no-queue(0), stdin(1), argument(2), argument-spacer(3), argument-end(4). During the reset vector, a program should be able to query the type port and get a null byte when there is no arguments to be expected, a 1 when arguments are present.
uxncli file.rom arg1 arg2
^ ^^ ^
2 32 4
The Console/read port is used to listen to incoming bytes during a Console vector event.
@on-reset ( -> ) ;on-console .Console/vector DEO2 BRK @on-console ( -> ) .Console/read DEI .Console/write DEO #20 .Console/write DEO BRK
The Console/write port is used to send data through the console. For example, a program sending the text "Hello", will trigger the console's port 5 times; once for each character.
@on-reset ( -> ) ;text &while ( -- ) LDAk .Console/write DEO INC2 LDAk ?&while POP2 BRK @text "Hello $1
Screen Device
| screen | 20 | vector* | 28 | x* |
|---|---|---|---|---|
| 21 | 29 | |||
| 22 | width* | 2a | y* | |
| 23 | 2b | |||
| 24 | height* | 2c | addr* | |
| 25 | 2d | |||
| 26 | auto | 2e | pixel | |
| 27 | -- | 2f | sprite |
|20 @Screen/vector $2 &width $2 &height $2 &auto $2 &x $2 &y $2 &addr $2 &pixel $1 &sprite $1
The Screen/vector* is evaluated 60 times per second. The screen device is capable of displaying graphics in any four colors, which are defined by the system device. The screen is made of two selfstanding layers, the foreground layer treats color0 as transparent.
@on-reset ( -> ) ;on-screen .Screen/vector DEO2 BRK @on-screen ( -> ) [ LIT &frame $1 ] INCk ,&frame STR #01 AND ?&>skip ( 30 times per second ) &>skip BRK
The Screen/width* and Screen/height* ports can be set to resize the screen for systems that supports it, but reading from these ports should ALWAYS return the actual sizes, as programs make use of the screen sizes to position responsive graphics on the screen.
@on-reset ( -> ) set-theme .Screen/width DEI2 #01 SFT2 .Screen/x DEO2 .Screen/height DEI2 #01 SFT2 .Screen/y DEO2 #01 .Screen/pixel DEO ( paint a black pixel, on background ) #43 .Screen/pixel DEO ( paint a red pixel, on foreground ) #93 .Screen/pixel DEO ( fill a red rectangle bottom-left, on background ) #e2 .Screen/pixel DEO ( fill a cyan rectangle top-right, on foreground ) BRK
The Screen/pixel port defines the pixel or fill mode, layer to draw on, optional horizontal/vertical flipping of the quadrant to fill, and which of the four colors to use. When the fill bit is active, the operation will fill a portion of the screen starting at the x,y position until the edges of the screen. The default quadrant is bottom-right, flipping the x bit will fill the buttom-left quadrant, and so on.
M L Y X 3 2 1 0 Fill ----+ | | | | | | +---- Color bit 0 Layer ------+ | | | | +------ Color bit 1 Flip Y --------+ | | +-------- Unused Flip X ----------+ +---------- Unused
The Screen/sprite port defines the 1-bit or 2-bit mode, layer to draw on, optional horizontal and vertical flipping of the sprite, and the colors to use.
M L Y X 3 2 1 0 2bpp ----+ | | | | | | +---- Color bit 0 Layer ------+ | | | | +------ Color bit 1 Flip Y --------+ | | +-------- Color bit 2 Flip X ----------+ +---------- Color bit 3
The 8x8 sprite data to draw is specified by writing its location in memory to the Screen/addr* port.
@on-reset ( -> ) set-theme #0020 .Screen/x DEO2 #0040 .Screen/y DEO2 ;sprite-icn .Screen/addr DEO2 #01 .Screen/sprite DEO BRK @sprite-icn 00 66 ff ff ff 7e 3c 18 ( . . . . . . . . 00 . # # . . # # . 66 # # # # # # # # ff # # # # # # # # ff # # # # # # # # ff . # # # # # # . 7e . . # # # # . . 3c . . . # # . . . 18 )
| 0 | 4 | 8 | c | ||||
|---|---|---|---|---|---|---|---|
| 1 | 5 | 9 | d | ||||
| 2 | 6 | a | e | ||||
| 3 | 7 | b | f |
The color nibble defines which color is drawn for each pixel of a sprite. The following table presents all possible combinations, assuming a sprite has a background of value 0 and three concentric circles of values 1, 2, and 3 (counting from the outside). For 1-bit sprites, only values 0 and 1 are applicable.
c = !ch ? (color % 5 ? color >> 2 : 0) : color % 4 + ch == 1 ? 0 : (ch - 2 + (color & 3)) % 3 + 1;
blending[4][16] = {
{0, 0, 0, 0, 1, 0, 1, 1, 2, 2, 0, 2, 3, 3, 3, 0},
{0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3},
{1, 2, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1, 1, 2, 3, 1},
{2, 3, 1, 2, 2, 3, 1, 2, 2, 3, 1, 2, 2, 3, 1, 2}};
The Screen/auto port automates the incrementation of the position and sprite address whenever a drawing command is sent, so the program does not need to manually move to the next sprite, or the next position.
3 2 1 0 * A Y X Length bit 3 ----+ | | | | | +---- Auto X Length bit 2 ------+ | | | +------ Auto Y Length bit 1 --------+ | +-------- Auto Addr Length bit 0 ----------+
The length nibble controls the number of extra sprites that will be drawn in addition to the first, each time the sprite port is written to. When set to 0, only a single sprite will be drawn. As many as 16 sprites can be drawn at once with a value of 15.
The auto-x and auto-y bits control the increment of the x and y ports respectively, as well as the layout of the extra sprites drawn when the length nibble is not null. The extra sprites are drawn as columns moving rightward for auto-x, and rows moving downward for auto-y. If the flip bits of the sprite byte are set, the directions are reversed. If the auto-addr bit is set, the address port will be incremented for each sprite drawn by increments of 8 for 1-bit, and by increments of 16 for 2-bit.
@paint-sprite ( x* y* -- ) .Screen/y DEO2 ( set y position ) .Screen/x DEO2 ( set x position ) #16 .Screen/auto DEO ( set length 2 with auto y and addr ) ;23x-icns .Screen/addr DEO2 ( set addr ) #01 .Screen/sprite DEOk DEOk DEO ( draw 3 rows of 1-bit sprites ) JMP2r @23x-icns ( 0 ) ( 1 ) ( 0 ) 0010 2847 2810 0001 0000 00e0 2040 8000 ( 1 ) 0204 080f 0000 0001 0000 00e0 2040 8000 ( 2 ) 0204 080f 0000 0000 0010 28c4 2810 0000
Audio Device
| audio | 30 | vector* | 38 | adsr* |
|---|---|---|---|---|
| 31 | 39 | |||
| 32 | position* | 3a | length* | |
| 33 | 3b | |||
| 34 | output | 3c | addr* | |
| 35 | -- | 3d | ||
| 36 | -- | 3e | volume | |
| 37 | -- | 3f | pitch |
|30 @Audio0/vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 |40 @Audio1/vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 |50 @Audio2/vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1 |60 @Audio3/vector $2 &position $2 &output $1 &pad $3 &adsr $2 &length $2 &addr $2 &volume $1 &pitch $1
The Audio/vector* is evaluated when a note ends. All samples used by the audio devices are unsigned 8-bit mono.
When a byte is written to the Audio/pitch port, any sample that is currently playing will be replaced with the sample defined by all the values set in the device, it starts playing an audio sample from memory located at Audio/addr* with length of Audio/length*. It loops the sample until it reaches the end of the envelope defined by Audio/adsr*. Several fields contain more than one component:
| ADSR* | Pitch | Volume | ||||||
|---|---|---|---|---|---|---|---|---|
| Subfield | Attack | Decay | Sustain | Release | Loop | Note | Left | Right |
| Size (bits) | 4 | 4 | 4 | 4 | 1 | 7 | 4 | 4 |
Each of the ADSR components is measured in 15ths of a second, so writing #ffff to Audio/adsr* will play a note that lasts for exactly four seconds, with each section of the envelope lasting one second. If Audio/adsr* is #0000 then no envelope will be applied: this is most useful for longer samples that are set to play once by setting the most significant bit of Audio/pitch to 1.
The envelope varies the amplitude as follows: starting at 0%, rising to 100% over the Attack section, falling to 50% over the Decay section, remaining at 50% throughout the Sustain section and finally falling to 0% over the Release section. The envelope is linearly interpolated throughout each section.
@on-reset ( -> ) #0248 .Audio/adsr DEO2 #88 .Audio/volume DEO ;note-pcm .Audio/addr DEO2 #0010 .Audio/length DEO2 #30 .Audio/pitch DEO BRK @square-pcm ff ff ff ff ff ff ff ff 00 00 00 00 00 00 00 00
The two Audio/volume components balance how loudly the next sample will play in each ear. #ff sets maximum volume for both speakers. While the sample is playing, the Audio/output byte can be read to find the loudness of the envelope at that moment.
Controller Device
| controller | 80 | vector* | 88 | -- |
|---|---|---|---|---|
| 81 | 89 | -- | ||
| 82 | button | 8a | -- | |
| 83 | key | 8b | -- | |
| 84 | player | 8c | -- | |
| 85 | -- | 8d | -- | |
| 86 | -- | 8e | -- | |
| 87 | -- | 8f | -- |
|80 @Controller/vector $2 &button $1 &key $1
The Controller/vector* is evaluated when a button, is pressed or released, and when a key is pressed. The key port is set to zero after the vector is called.
0x01 | A Ctrl | 0x10 | Up |
0x02 | B Alt | 0x20 | Down |
0x04 | Select Shift | 0x40 | Left |
0x08 | Start Home | 0x80 | Right |
The Controller/button port works similarly to an NES controller, where there the state of each one of the 8 buttons is stored as a bit in a single byte.
@on-reset ( -> ) ;on-controller .Controller/vector DEO2 BRK @on-controller ( -> ) .Controller/button DEI DUP #20 AND ?on-dpad-down DUP #80 AND ?on-dpad-right POP BRK @on-dpad-down ( button -> ) @on-dpad-right ( button -> )
The Controller/key port holds the character data that is pressed during the vector event.
@on-controller ( -> ) .Controller/key DEI DUP ?on-key POP BRK @on-key ( key -> )
Would the need for multi-player games arise, the Controller/player ports can hold the id of the player sending the controller event.
Mouse Device
| mouse | 90 | vector* | 98 | -- |
|---|---|---|---|---|
| 91 | 99 | -- | ||
| 92 | x* | 9a | scrollx* | |
| 93 | 9b | |||
| 94 | y* | 9c | scrolly* | |
| 95 | 9d | |||
| 96 | state | 9e | -- | |
| 97 | -- | 9f | -- |
|90 @Mouse/vector $2 &x $2 &y $2 &state $4 &scrollx $2 &scrolly
The Mouse/vector* is evaluated when the mouse is moved and when a button is pressed or released.
@on-reset ( -> ) ;on-mouse .Mouse/vector DEO2 BRK @on-mouse ( -> ) .Mouse/state DEI ?&down BRK &down ( -> ) .Mouse/x DEI2 print-dec LIT ", .Console/write DEO .Mouse/y DEI2 print-dec #0a .Console/write DEO BRK
The Mouse/state port holds a byte in which each bit is a button state. The byte value of holding down the mouse1 button is 01, and holding down mouse1+mouse3 button is 05. The Mouse/scroll-x* and Mouse/scroll-y* ports hold signed shorts, either ffff and 0001, for -1 and +1, after firing the vector on a scrolling event, the ports are both immediately set to 0.
File Device
| file | a0 | vector* | a8 | name* |
|---|---|---|---|---|
| a1 | a9 | |||
| a2 | success* | aa | length* | |
| a3 | ab | |||
| a4 | stat* | ac | read* | |
| a5 | ad | |||
| a6 | delete | ae | write* | |
| a7 | append | af |
|a0 @File1/vector $2 &success $2 &stat $2 &delete $1 &append $1 &name $2 &length $2 &read $2 &write $2 |b0 @File2/vector $2 &success $2 &stat $2 &delete $1 &append $1 &name $2 &length $2 &read $2 &write $2
The File/vector* is normally unused, but is reserved for systems where a portable data format(disk, etc..) can be inserted. There is no specs for disk handling at this time.
When File/name* resolves to a file, writing an address to File/read* will write the file's data at that address up to the length in File/length*, the length of data read is then put in File/success*. If the file is longer, subsequent writes to File/read* will read the next chunk of data into the memory region, so it is possible to read the contents of very large files one chunk at a time.
@on-reset ( -> ) ;filename .File/name DEO2 #0010 .File/length DEO2 ;buffer .File/read DEO2 BRK @filename "in.txt $1 @buffer $10
When File/name* resolves to a directory, writing the address to File/read* will read the directory content in the following format: Each file or directory is on its own line, prefixed with a four characters for the file details, followed by a space, the file's name and a linebreak.
001a file.txt ???? large file.mp4 ---- directory/
The file details part of the directory listing line can also be obtained via the File/stat* port, the File/length* specifies the length of the buffer to write to. A file size is padded with zeroes and will always include the lowest nibbles. The length of the details will always fill the requested length whether it is with zeros for a file size, or with dashes for the directory.
0-fA file-A directory?A file that is larger than 64kb!A missing file
@is-folder ( name* -- bool ) .File/name DEO2 #0001 .File/length DEO2 ;&b .File/stat DEO2 [ LIT2 &b $1 "- ] EQU JMP2r
Writing files is performed by writing to File/write* will write the data in memory at that address up to the length in File/length*, the length of data writte is then put in File/success*. If File/append is set to 0x01, then the data in the memory region will be written after the end of the file, if it is 0x00 it will replace the contents of the file.
If the file doesn't previously exist then it will be created. File/success* will be set to the length written. As with reading files and directories, subsequent writes to File/write* will write more chunks of data to the file.
@on-reset ( -> ) ;filename .File/name DEO2 #0005 .File/length DEO2 ;data .File/write DEO2 BRK @filename "out.txt $1 @data "hello $1
Finally, to delete a file, write any value to the File/delete byte, if successful, File/success* will be set to 0x0001, otherwise 0x0000. Writing to File/name* closes the previously opened file or directory. The device may not access files outside of the working directory. When reading or writing, if the length of data required exceeds the working memory(0xffff), the length is set to the maximum available.
Datetime Device
| datetime | c0 | year* | c8 | doty |
|---|---|---|---|---|
| c1 | c9 | |||
| c2 | month | ca | isdst | |
| c3 | day | cb | -- | |
| c4 | hour | cc | -- | |
| c5 | minute | cd | -- | |
| c6 | second | ce | -- | |
| c7 | dotw | cf | -- |
|c0 @DateTime &year $2 &month $1 &day $1 &hour $1 &minute $1 &second $1 &dotw $1 &doty $2 &isdst $1
All the ports are zero-based, the dotw port begins on sunday[0].
@print-date-num ( -- ) .DateTime/year DEI2k print-dec [ LIT "- ] .Console/write DEO INC INC DEIk INC print-dec-pad [ LIT "- ] .Console/write DEO INC DEI !print-dec-pad
A collection of Varvara application support standards.
This is a collection of various libraries used in the Varvara roms.
Snarf files are the clipboard standard for Varvara software.
A .snarf file contains a clipboard buffer, programs will handle clipboard operations by writing to that external invisible file. To quickly write to the snarf file before pasting:
cat > .snarf
The cut operation deletes the selected text from the screen and puts it in the snarf buffer, snarf copies the selected text to the buffer without deleting it, paste replaces the selected text with the contents of the buffer.
|0100 @snarf/length @snarf/copy ( data* length* -- ) ;&path .File/name DEO2 .File/length DEO2 .File/write DEO2 JMP2r @snarf/paste ( addr* -- length* ) ;&path .File/name DEO2 ;&length .File/length DEO2 ;&buf .File/read DEO2 .File/success DEI2 JMP2r &buf $&length
Snarf is the term for Copy on the Plan9 operating system. The operation is not to copy but to snarf. It's called snarf because snarf is what it does. There is no design document.
Symbol files are debug information about an assembled rom.
A symbols(*.tal.sym) file is a binary format that contains a series of symbols in the format: an absolute addresses, where the first byte is the most significan byte, the second is the least significant byte, followed by a null-terminated string. They are generated by Uxntal assemblers, such as Drif, and are used by debugging tools, such as uxndis and Beetbug, which do not otherwise have access to a program's labels.
0140 on-draw 0142 on-draw/loop 01a8 clip-tile 01f2 ...
- Symbols Viewer, written in Uxntal.
Metadata exposes additional information about a Varvara rom.
A program's metadata can be found by forming a 16-bit address from the second and third bytes of a compatible rom file. A compatible rom must begin with a0, a two bytes address, and the subsequent 3 bytes should always be 80 06 37.
| First six bytes | ||||||
|---|---|---|---|---|---|---|
| LIT2 | address | LIT | port | DEO2 | ... | |
| a0 | hb | lb | 80 | 06 | 37 | ... |
The address corresponds to the absolute location of the metadata, once loaded in memory, so it has a 0x100 bytes offset(the zero-page) which needs to be accounted for when accessing the metadata externally.
The body of the metadata can be anything, but it is recommended that it be plain-text with 0a as line separator, and that the first line be the name of the application. Here are some suggestions for lines to include in the body of the metadata:
- name: application name
- version: release id
- description: what it is
- author: who built it
The metadata is stored in the rom itself to allow the program to make use of this information internally. The entire size of the metadata should be at most 256 bytes.
Implementation
The metadata format begins with a byte for the varvara version, followed by a null-terminating text:
|0100 ( -> ) ;meta #06 DEO2 BRK @meta 00 &body ( name ) "Nasu 0a ( details ) "A 20 "Sprite 20 "Editor 0a ( author ) "By 20 "Hundred 20 "Rabbits 0a ( date ) "Jan 20 "8, 20 "2023 00 02 ( icon ) 83 =appicon ( mask ) 41 1705
If supported by the emulator, writing the metadata's address to the system device, via #06 DEO2, informs the emulator of the metadata's location, and it may choose to handle this information when a ROM is started.
Extended
The length of the metadata can be extended and host various other informations. The format for the fields is: a single byte id field, followed by a 2 bytes value field.
| Application | id | value |
|---|---|---|
| -- | 41 | Expected device mask. |
| Potato | 83 | 24x24 icon in the chr format. |
| 88 | 64x64 icon in the icn format. | |
| a0 | The application manifest. |
Reading Metadata
A program can check a rom for metadata with the following routine:
@has-meta ( filename* -- bool ) .File/name DEO2 #0003 .File/length DEO2 ;&b .File/read DEO2k DEO2 ,&litport LDR2 #8006 EQU2 ,&deo2 LDR #37 EQU AND JMP2r &b &litport $2 &deo2 $1
- Metadata Viewer, written in Uxntal.
Manifest exposes a rom's features.
A manifest is a simple data structure that allows Varvara applications to specify what feature can be reached by shortcuts.
The manifest.tal is a support library created to this end. It is also used in the definition of applications in Potato.
@manifest 04 "Application $1 01 'n :file-new "New $1 01 'r :file-rename "Rename $1 01 'o :file-open "Open $1 01 's :file-save "Save $1 03 "Edit $1 01 'c :edit-copy "Copy $1 01 'v :edit-paste "Paste $1 01 'x :edit-cut "Cut $1 02 "Select $1 00 1b :select-reset "Reset $1 01 'a :select-all "All $1 $1
Indental is a plaintext dictionary-type database format designed to store elements accessible by name.
In the Indental file, an unindented line declares the key to a new root node, indented lines associate parameters or lists to their root node, a key-value pair is declared with a colon, and a list is declared by a sequence of equally indented lines.
NAME KEY : VALUE LIST ITEM1 ITEM2
Or, {NAME:{KEY:VALUE,LIST:[ITEM1,ITEM2])}
This database format is used to store the plain-text content of the Oscean wiki.
17P09— Varvara Specification Release