THIS IS STILL A DRAFT: suggestions for improvements before release are welcome.
This is the specification for the RMV file format.
"I" in this document refers to Thomas Kolar aka ralokt, but the format was originally designed by Christoph Nikolaus. It has since been expanded upon by me and Elias Gailberger aka KharadBanar.
Part of the design decision section is opinionated, but the spec section aims not to be.
RMV v2 is a backwards-incompatible update of the format.
- removed utf-8 property. All text data is now always utf-8.
- removed text-based result string.
- removed timestampchange event type.
- added bbbv_low and bbbv_high properties (low and high bits of bbbv).
- added square_size property.
- mouse coordinates are now signed, and relative to the top left corner of the board, as opposed to the top left corner of the UI.
- added machine-readable fields to specify the clone used, along with its major version.
- added named extension properties. Clones that want to add metadata that is not part of the specification may put it here, with the goal that if they are found useful, they are added to the format in later versions.
- added EVF game modes
Responsibility for making changes to this format rests with ralokt
, or
KharadBanar
should ralokt
get hit by a bus or otherwise be dead or
incapacitated. From now on, that person is referred to as the RIDAA (RMV ID
assignment authority), and both (if available) together form the RSC (RMV
specification committee).
Note: We're not actually formally founding an organization, we just want to use the abbreviations above when discussing how changes to the file format will be handled.
- E-Mail: [email protected]
- Discord: @ralokt
- E-Mail: [email protected]
First and foremost, by "alternatives", I mean EVF.
MVR is not an open format, and if you're considering AVF, do yourself a favor and reconsider. In particular, while RMV makes some different tradeoffs/design decisions, EVF is superior to AVF in pretty much every way, and there is no reason to use AVF over it.
The EVF spec can be found here:
https://github.com/eee555/ms-toollib/blob/main/evf%20format%20(evf%E6%A0%87%E5%87%86).md
The original design goals aren't quite known, as Christoph Nikolaus, the original author of the format, has resigned. However, we can infer the following:
- RMV is meant to be a universal format (currently, it's only being used by vsweep)
- RMV is meant to be usable for the official world ranking (and it is)
- RMV prioritizes ease of use over file size (it stores board events alongside mouse events - see below for details)
Unlike AVF or EVF, RMV also contains board events - events that describe how the state of the game changed in response to the the previous mouse event.
This approach has a few advantages:
-
It makes writing replay viewers extremely simple. All other formats require implementing a Minesweeper engine.
-
It documents a game as it was played, even if the implementation was buggy. For example, an implementation with a bug like rilian clicks would still produce replays that show the game as it was played, not as it should have been played if the implementation hadn't been buggy.
Of course, with formats that don't store board events, it's still possible to parse the version info to account for bugs in specific versions; but now developers don't just have to implement a Minesweeper engine, but also make it conditionally support buggy behavior.
-
Well-behaved clones implementing RMV are essentially factories for test data for new Minesweeper engines, as they document the correct result for every action in every game that is played and saved on them.
It also has some disadvantages:
- File size is increased. While by far the most events in Minesweeper replays are mouse move events, it's not insignificant, either.
- While there are upsides to this, as outlined above, the format contains redundant information.
EVF does not contain board events - this is the greatest conceptual difference between the two formats.
EVF supports a UUID field that may contain a device UUID for the device the game was played on.
This feature isn't a reliable measure against fake replays, as the UUID is by definition stored in any videos a user publishes, and can be spoofed.
For less savvy users, the UUID links games to a specific device. That includes, say, games played on a company computer during a meeting that could have been an email, and has privacy implications in general.
Besides, asymmetric encryption would be a better solution for the problem the UUID field aims to solve.
It would be great if EVF and RMV converged more in general, but this is why RMV makes an explicit decision not to implement that.
Starting with RMV version 2, all text data is explicitly specified to contain utf-8. Parsers can and should reject replays that don't fulfil this requirement.
This makes writing (proper) parsers easier. It also means that for any translated replays, any text data needs to be converted.
The structure is given as a list. Each list item contains one of the following:
- A definition consisting of a value, followed by "->", followed by a name,
where values are one of:
chars(<string_literal>)
Just these characters. For example:chars("asdf")
.uint(<length>)
- an unsigned integer encoded inlength
bytes.sint(<length>)
- a signed integer encoded inlength
bytes.str(<length>)
- a utf-8 string encoded inlength
bytes, wherelength
is given as the name of a previously parsed result.blob(<length>)
- a binary blob that islength
bytes long, wherelength
is given as the name of a previously parsed result- an expression using previously defined values
if <condition>
- makes following indented list conditionalrepeat <amount> times
-<amount>
-fold repetition of the following indented list, whereamount
is given as the name of a previously parsed resultrepeat until <name> defined
- repetition of the following indented list until a value is defined asname
Conditions, string literals, and expressions are loosely defined as pseudocode in the interest of sacrificing theoretical precision for readability.
In the interest of keeping the format definition compact, its structure will be defined first, and semantics of each value will get their own paragraph later.
"*rmv"
->file_signature
uint(2)
->file_type
if file_type == 2
:uint(1)
->clone_id
if file_type == 2
:uint(1)
->major_version_of_clone
uint(4)
->file_size
if file_type == 1
:uint(2)
->result_str_size
uint(2)
->version_info_size
uint(2)
->player_info_size
uint(2)
->board_size
uint(2)
->preflagged_size
uint(2)
->properties_size
if file_type == 2
:uint(2)
->extension_properties_size
uint(4)
->vid_size
uint(2)
->checksum_size
if file_type == 1
:str(result_str_size)
->result_str
str(version_info_size)
->version_info
uint(2)
->num_player_fields
repeat num_player_fields times
uint(1)
->player_field_size
str(player_field_size)
->player_field
uint(4)
->timestamp_boardgen
uint(1)
->cols
uint(1)
->rows
uint(2)
->num_mines
repeat num_mines times
uint(1)
->col
uint(1)
->row
if preflagged_size != 0
:uint(2)
->num_preflags
repeat num_preflags times
uint(1)
->col
uint(1)
->row
repeat properties_size times
uint(1)
->property
if file_type == 2
uint(2)
->num_extension_properties
repeat num_extension_properties times
uint(1)
->extension_property_name_size
str(extension_property_name_size)
->extension_property_name
uint(1)
->extension_property_value_size
blob(extension_property_value_size)
->extension_property_value
repeat until time_ms defined
uint(1)
->event_code
if file_type == 1 && event_code == 0
event_code
->timestampchange_event_type
uint(4)
->timestamp
if 1 <= event_code <= 7
event_code
->mouse_event_type
uint(3)
->gametime
uint(1)
->nFlags
if file_type == 1
uint(2)
->xpos
uint(2)
->ypos
else
sint(2)
->xpos
sint(2)
->ypos
if 9 <= event_code <= 14 or 18 <= event_code <= 27
event_code
->board_event_type
uint(1)
->col
uint(1)
->row
if 15 <= event_code <= 17
event_code
->termination_event_type
uint(3)
->time_ms
blob(checksum_size)
->checksum
This is just the file signature/magic number for RMV.
The format version.
Allowed values:
1
(original RMV format)2
(RMV2 format)
This will be changed when the file format receives a backwards-incompatible update.
A unique ID for the clone that produced this replay, assigned by the RIDAA.
Clones that don't have an assigned value yet MUST set this value to 0
and MUST
use the clone_name
extension property instead (see below).
Clones that have an assigned value MUST use that value, and MUST NOT set the
clone_name
extension property.
This value is used to disambiguate extension properties. Since clones can define their own extension properties, collisions can be resolved using this field. It also serves as a machine-readable field for the clone that produced the replay.
Fixed values:
0
(unspecified)1
(vsweep)- Any other value, may have been assigned later (degrade to parsing universal data)
The following values are assigned proactively, but may still be reassigned. If
newer versions of these clones want to write RMV replays, they can use these
values right away, skipping the step of using the clone_name
extension
property. They should inform the RIDAA that they are claiming their preassigned
ID, fixing it forever.
2
(arbiter)3
(MSX)4
(clone)5
(metasweeper)6
(freesweeper)7
(mysweeper)
If the RIDAA ever runs out of IDs, they may reassign an unclaimed ID, should the developers of the respective clones not be reachable/responsive.
Clone authors may request an ID from the RIDAA. Depending on demand, the RIDAA may ask for proof that a project is being seriously worked on/in an advanced stage.
In order to be eligible, clones should aim to be suitable for use in the official world ranking on minesweepergame.com . This is a loose requirement:
- Clones don't need to be usable on the world ranking yet
- Clones don't even need to have all of the necessary features yet However, clones should not be fundamentally incompatible with being used on the world ranking.
The RIDAA (current and future) shall not cite any considerations besides the above as reasons for not assigning an ID to a project. They exist as a mechanism to prevent different projects from claiming the same ID, and to ensure that the limited space of 255 IDs isn't exhausted without reason. They do NOT exist as gatekeepers, and their goal shall be to get projects an ID assigned as soon as possible, regardless of any personal considerations.
Fixed IDs will never be reassigned. Instead, if necessary, a new version of the format will be released.
The major version of the clone that generated the replay.
It's recommended to increment this field whenever parsers could benefit from telling the difference between this version and the previous version - for example, when new extension properties are assigned, or when a bug is fixed/a feature is added that affects replay generation.
It's recommended to release new major versions whenever the last paragraph applies, or to make the contents of this field align with such changes instead of the actual major version number.
Size of the entire file, in bytes.
Size of result_str
, in bytes. Only present in version 1 of the format.
Size of version_info
, in bytes.
Size of player_info
, in bytes.
Size of board
section, in bytes. The board
section contains:
timestamp_boardgen
cols
rows
num_mines
- The
col, row
pairs that define the positions of the mines.
See below for details.
Size of preflagged
section, in bytes. The preflagged
section contains:
num_mines
- The
col, row
pairs that define the positions of the mines.
See below for details.
Size of properties
section, in bytes. Also equal to the number of properties,
as properties are 1 byte each.
See below for details.
Size of extension_properties
section, in bytes. Only present in version 2 of
the format.
See below for details.
Size of the vid
section, in bytes. The vid
section contains the events.
Size of checksum
, in bytes.
See below for details.
Result string. Contains metadata about the game in text format.
Only present in version 1 of the format, so it will only be documented briefly and informally.
Contains "\n" (<key> ":" <value> "#")* "\n"
. The keys that may be available
are:
LEVEL
SCORE
NAME
NICK
3BV
NF
TIMESTAMP
Of these, only 3BV
is interesting for modern parsers, as it isn't available
elsewhere in the format in version 1.
A free-form human-readable version information string.
The number of player fields. See below.
Size of the next player field, in bytes.
Contents of the next player field.
Player fields are positional.
The player fields are:
name
: Intended for the player's full/legal namenickname
: Intended for the player's nickname/online handle/however they want to be addressed in an online context.country
: The player's country as a free-form text field.token
: A secret that proves that the game was played by a player knowing that secret. For example, a Scoreganizer tournament key.
Fields are assumed to be empty if not present.
If you are implementing RMV, do not add your own player fields; instead, add an extension property (see below).
The timestamp at which the board was generated.
The number of columns, rows, and mines for the board the game was played on.
The column and row the next mine was placed in.
How many flags were placed before the game started.
The column and row the next preflag was placed in.
Properties are positional. In both format versions, the first 4 properties are:
marks
- whether questionmarks were enablednf
- whether the game was a non-flagging gamemode
- the game mode:0
- normal1
- upk2
- cheat3
- density4
- win75
- competitive_solvable6
- strong_solvable7
- weak_solvable8
- to_be_solvable9
- strong_guessable10
- weak_guessable11
- chording_recursive_standard12
- flag_recursive13
- chording_flag_recursive
level
- the level played:0
- beginner1
- intermediate2
- expert3
- custom
Values for mode
that are larger than 3 are only valid in version 2 of the
format.
In version 1, the following extra property may exist:
utf8
- whether or not text strings in the replay file are encoded as utf-8. if the property doesn't exist, the encoding isn't defined. (In version 2, the encoding must always be utf-8, and this property no longer exists.)
In version 2, the following extra properties exist:
bbbv_low
- low 8 bits of the 3bv of the boardbbbv_high
- high 8 bits of the 3bv of the boardsquare_size
- side length of squares the game was played on, in pixels
If you are implementing RMV, do not add your own player fields; instead, add an extension property (see below).
The number of extension properties. Only present in version 2 of the format.
Extension properties are discussed in their own section below.
Size of the following extension property name, in bytes. Only present in version 2 of the format.
Extension properties are discussed in their own section below.
The next extension property name. Only present in version 2 of the format.
Extension properties are discussed in their own section below.
Size of the following extension property value, in bytes. Only present in version 2 of the format.
Extension properties are discussed in their own section below.
The next extension property value. Only present in version 2 of the format.
Extension properties are discussed in their own section below.
Determines the type of the next event. The exact meanings are described in the
*_event_type
sections below.
Only valid in version 1 of the format, and only used in very old replays. Always
0
if present.
New current timestamp. Only present in version 1 of the format.
Mouse events - moves, button presses, and button releases.
Allowed values and meanings:
1
- mouse move2
- lmb_down3
- lmb_up4
- rmb_down5
- rmb_up6
- mmb_down7
- mmb_up
SHOULD simply correspond 1:1 to mouse events that happened ingame. MUST do so if the middle mouse button was not used.
Time since start of the game, in milliseconds.
Bitflags describing which mouse buttons were pressed. TODO: describe in detail.
x and y positions of the cursor at the time of the event, in pixels.
In format version 1, both are unsigned, and relative to the top left corner of
ViennaSweeper's client area. xpos
is offset by 12
, while ypos
is offset by
56
.
In format version 2, both are signed, and relative to the top left corner of the board.
Board events - a square changing what it displays.
Allowed values and meanings:
9
- pressed10
- pressed questionmark11
- closed12
- qm13
- flag14
- open (works as open_blast)18
- open_019
- open_120
- open_221
- open_322
- open_423
- open_524
- open_625
- open_726
- open_827
- open_blast
Board coordinates of the square that was affected.
Termination events - represent the game ending.
Allowed values and meanings:
15
- blast16
- win17
- other - when the game ended in neither a win or a blast - system crash, network failure, accidentally clicking the smiley, ...
Gametime, in thousandths, since the start of the game. Technically part of the
termination event, and included in vid_size
. However, since it marks the end
of the game, can be seen as a standalone field that contains the total game
time.
A checksum that is calculated based on the rest of the replay file. Should not contain information from:
file_signature
file_type
clone_id
major_version_of_clone
file_size
Properties and player fields are positional. This means that if clone authors decided to extend the format independently, a situation could arise where different clones interpret different properties/player fields differently.
Therefore, clone authors that are not members of the RSC MUST NOT define new properties. However, they can define extension properties.
Unlike properties and player fields, extension properties are named - they
contain a name as well as a value. They are also intended to be interpreted in
tandem with the clone_id
, as opposed to properties and player fields, which
are intended to be universal. In other words: extension properties being named
makes collisions less likely, and clone_id
provides a fallback.
Extension properties are defined as blobs. This is so that they can contain actual binary data, for example, integer values. However, if they contain text data, it MUST be encoded in utf-8.
The following extension properties are reserved:
clone_name
- MUST NOT be set by clones that have an assignedclone_id
. MUST be set by clones that don't have an assignedclone_id
.- All properties starting with
rmv_
We suggest that clone authors use the following conventions when defining extension properties:
- In general: follow the convention
lowercase_with_underscores
- For statistics: prefix with
stat_
. If the statistic is an abbreviation, use the lowercase version of that, otherwise, use the full name.- Examples:
stat_islands
,stat_openings
,stat_stnb
,stat_hzini
- Examples:
- For fields that relate to the player and could become player fields in the
future: prefix with
player_
.- Examples:
player_signature
,player_saolei_id
,player_mouse_model
- Examples:
- For fields that could become a universal property: prefix with
prop_
- Examples:
prop_show_arena_ticket_drops
,prop_rilian_clicks
,prop_openings_destroy_flags
,prop_elmar_technique
- Examples:
- For fields that are specific to your clone, prefix with
<clone_name>_
.- Examples:
vsweep_skin
,msx_skin
,arbiter_clipcursor
,metasweeper_uuid
- Examples:
More properties and player fields may be added to version 2 of the format by the RSC, if doing so is possible without breaking backwards compatibility.
This means:
- Parsers should read the properties they understand and ignore the rest
- Updates to the format must not render the previous point problematic
An example for a backwards compatible change is the new token
player field
that was added with ViennaSweeper 4.0.0. It contains extra information that can
be used to perform additional checks, but can be safely ignored to simply read a
replay file.
Backwards compatibility doesn't have to be perfect. For example, a RMV file with
the utf8
property would still have been interpreted by older versions of
ViennaSweeper as containing strings in latin1
, or some other native code page,
leading to some display errors. However, the RMV file would still have been
playable.
An example for a backwards-incompatible change to RMV is the new square_size
property. This is not backwards compatible, as unless square_size
is set to
16, replays in the new format would not be playable by a player that doesn't
understand this property - the mouse position relative to the board position
where changes happen would be incorrect.
In fact, square_size
being necessary to support arbitrary square sizes in
ViennaSweeper is the reason why RMV v2 is necessary.