The .blmod file format is designed as a replacement to the .blcm format, and to be generic
enough that it could be applied to the existing .bl3hotfix/.wlhotfix file format, or possible
future ones.
The main thing it's designed around is mods which consist of text files holding one "command" per line. Commands can be arbitrary strings (as long as they don't contain newlines). These commands can then be grouped into categories, which users can enable/disable to configure their mods - i.e. what BLCMM does.
This document only goes over the base file format. Tools interacting with it may define their own extensions.
For background info/early ideas which were incorporated into this spec, see this old proposal document.
A .blmod file is a yaml file, consisting of two separate documents.
'blmod':
'version': 1
'encoding': ascii
---
'category': root
'contains':
- 'enabled': |-
say This is a command
The first document is the file header, which contains overall information about the file. The second document contains all the mod content.
Using a yaml-based format allows us to use multi-line strings to store commands in line. This does rely on a few assumptions on how commands are parsed:
- Leading whitespace is ignored
- No command starts with a
'or-
This has not been found to be a problem in practice.
.blmod files do not use any "advanced" yaml features such as tags. Each document has a one-to-one
mapping to json, and can be round-trip converted (though of course it's not valid in json format).
The documents within the file can be validated against the following schema. You can convert both the document and the schema to json to use a standard json schema validator
Header
title: blmod header
description: .blmod file header
type: object
properties:
blmod:
description: >-
A magic string which MUST appear at the top of the file. The value is ignored.
encoding:
description: >-
What encoding the rest of the file uses.
While we have some predefined values (which SHOULD be case insensitive), any value is
acceptable. Custom values MUST also be written in ascii, to allow interpreting without fully
knowing the correct encoding. If a tool does not understand the encoding string, it MUST throw
an error.
anyOf:
- enum:
- ascii
- utf8
- utf16
- utf16le
- utf16be
- utf32
- utf32le
- utf32be
- type: string
pattern: ^[\x00-\x7F]+$
version:
description: >-
The file's format version. Parsers MUST always check this, and reject any value other than 1.
type: integer
minimum: 1
maximum: 1
games:
description: >-
An array of all games this mod file supports.
While we have some predefined values (which SHOULD be case insensitive), any value is
acceptable. Values which a tool does not recognise MUST be ignored.
type: array
minItems: 1
items:
anyOf:
- enum:
- bl1
- bl1e
- bl2
- tps
- aodk
- bl3
- wl
- bl4
- type: string
metadata:
description: >-
Arbitrary user-defined file-specific metadata.
type: object
default: {}
required:
- blmod
- encoding
- version
- gamesContents
title: blmod contents
description: .blmod file contents
$ref: "#/definitions/category"
definitions:
comment:
description: >-
A comment.
Comments are only information for users, they MUST NOT be taken to have any meaning.
type: object
properties:
comment:
description: The comment string.
type: string
required:
- comment
enabled:
description: An enabled command.
type: object
properties:
enabled:
description: The command string. MUST be formatted as a multiline string.
type: string
required:
- enabled
disabled:
description: A disabled command.
type: object
properties:
disabled:
description: The command string. MUST NOT be formatted as a multiline string.
type: string
required:
- disabled
category:
description: >-
A nested category.
Categories have no inherent enabled/disabled status, instead they inherit it recursively from
their contents:
- If there are no 'enabled' commands, the category is considered to be disabled. This includes
the case where there are no recursive child commands at all.
- If there are some recursive child commands, and there are no 'disabled' commands, the
category is considered to be enabled.
- Otherwise, if there are recursive child commands, but there are both 'enabled' and
'disabled' commands, the category is considered to be partially enabled.
type: object
properties:
category:
description: The category's name.
type: string
contains:
description: This category's contents.
type: array
items:
oneOf:
- "$ref": "#/definitions/comment"
- "$ref": "#/definitions/enabled"
- "$ref": "#/definitions/disabled"
- "$ref": "#/definitions/category"
locked:
description: >-
When true, the category is locked. An editor MUST NOT allow changing the enabled status of
any child commands recursively.
type: boolean
default: false
mut:
description: >-
When true, the category contains mutually exclusive options. An editor MUST only allow
exactly one of this category's direct children to be enabled or partially enabled. All
other direct children MUST be disabled.
type: boolean
default: false
metadata:
description: >-
When a category is obtained by importing another mod file into this one, the metadata in
the importee's header SHOULD be copied to this.
The presence of this property MAY be used to infer that a category is an entire mod, which
might for example adjust styling.
Generally, metadata in this property SHOULD be ignored, in favour of that in this file's
header. In some cases there may be reason to check mod-specific metadata - e.g. an editor
might look for a mod's description and demo screenshots, and display those.
type: object
required:
- category
- containsBoth of these schema deliberately allow additional properties. Future updates to this spec may add
new ones, and older tools MUST accept and ignore them. If a new property requires parsing changes,
the version property in the header will be bumped, hence tools MUST reject any version they don't
understand.
Additionally, tools working with .blmod files can add their own custom properties. Any property
starting with an underscore _ is a custom property, no "official" properties will ever start with
one. Tools adding custom properties SHOULD generally prefix them using the tool's name - e.g. BLCMM
might define a _blcmm_custom_prop. As with new "official" properties, all tools MUST accept and
ignore any custom properties they don't understand.
Different games have different encoding requirements, so we can't simply require all .blmod files
use the same one. To handle this we have an encoding property in the header - but we have a bit of
a chicken-and-egg problem. In order to be able to parse the header to find out what encoding we
want, we need to know what encoding to read it as.
The way we resolve this is by parsing the header twice, the first time with a temporary encoding to find out the actual encoding. All encodings MUST be written in ascii, so that they won't get corrupted if the temporary encoding is wrong. In order to do this we must still match the actual encoding's width and endianness however.
Exact steps for determining encoding are as follows:
-
If the file begins with a unicode BOM, we can immediately identify it as that encoding.
Bytes Encoding EF BB BFutf8 FF FEutf16le FE FFutf16be FF FE 00 00utf32le 00 00 FE FFutf32be All parsers MUST recognise all forms of the BOM, to make sure they don't try read the rest of the file using the wrong width/endianness. Parsers MAY however only implement some of these, and simply throw an error on encountering the other formats.
-
If there isn't a BOM, continue on to step 2.
-
If there is a BOM, select the relevant encoding as the temporary encoding.
The first 8 characters of any
.blmodfile MUST be the magic string'blmod':. Use the temporary encoding to read them out and validate this. If this is not the case, the file is considered not to be a.blmod, and a parser MUST NOT continue parsing it as one. Tools may choose to continue attempting to parse it as other file formats, this need not be a user facing error.If the prefix is valid, jump to step 4.
-
-
We did not have a unicode BOM. As mentioned above, the first 8 characters of any
.blmodfile MUST be the magic string'blmod':. Since these are all ascii, we can use them to determine width/endianness.Bytes Encoding 27 62 6c 6d ...Single byte 27 00 62 00 ...2-byte little endian 00 27 00 62 ...2-byte big endian 27 00 00 00 ...4-byte little endian 00 00 00 27 ...4-byte big endian As before, all parsers MUST recognise all these forms, however they MAY only implement some of them, and throw errors on the other formats. And if none of these match, the file is considered not to be a
.blmod, and a parser MUST NOT continue parsing it as one. -
Select an arbitrary encoding matching the detected width/endianness characteristics. This is the temporary encoding.
-
Seek back to the start of the file, then attempt to parse the first document.
If the parser encounters any undecodeable bytes (if the temporary encoding is wrong), it MUST NOT error. Instead, it MAY corrupt the given data in an arbitrary manner.
If yaml decoding fails, the file is ill formed and the parser MUST throw an error.
-
Read out the
encodingproperty, to get the actual encoding. This is an arbitrary (ascii) string, however we predefine the following formats (case-insensitively):asciiutf8utf16,utf16leutf16beutf32,utf32leutf32be
A parser MAY only implement some of these, it MAY accept other aliases to the same encodings, and it MAY accept other strings mapping to completely new encodings.
If a parser doesn't recognise the encoding (including if it's one of the predefined ones it chose not to implement), or if the encoding property is not present or of the wrong type, the file is ill formed and the parser MUST throw an error.
-
If the actual encoding is different to the temporary encoding, throw away all the data, seek back to the start of the file, and parse the header again using the actual encoding.
After determining the encoding, the file can be parsed using a standard yaml decoder.
If the file is not valid yaml, a parser MUST throw an error.
If the file parses, but does not fit the schema, a parser SHOULD throw an error. It is permitted not to if the mistake occurs in an inconsequential location - for example, if a file contains a tag, a parser may simply ignore it. It is also permitted to "best-effort" parse the rest of the file - for example a single malformed command might not invalidate the best of the file. Recovery in this scenario is implementation defined.
The version property in the header MUST be equal to the integer 1. If it is not, a parser MUST
throw an error, which SHOULD state that the file was made for a newer version.
Serializing a .blmod file is a little more involved.
Before writing anything, the serializer MUST update the encoding property to the encoding it's
outputting as.
Then there are a few yaml settings that must be configured:
- All properties MUST be quoted, using single quotes.
- All arrays/mappings MUST be in their multiline form (no json-style).
- All strings under an
enabledproperty MUST be multiline strings. A serializer MAY choose any multiline format, as is appropriate for the contained data. - All other strings MUST appear on a single line, even if they contain newlines.
- The serializer MUST NOT output any yaml tags.
Finally, the very first line of output MUST be the blmod property - all files MUST start with the
string 'blmod':.
A mod editor, or any tool that supports both parsing and serializing, has a few more round-tripping rules to obey (assuming the user hasn't made any changes).
A tool MAY change the following data - i.e. they're not required to round-trip:
- The
blmodproperty's value. - The predefined
encodingandgameenum's values - though their meanings SHOULD NOT be changed without user action. For example, normalizingbl1toBL1orutf-8toutf8is allowed. - The order of properties in any mapping. The
blmodproperty MUST still be the first property at the top of the file however. - Any custom property the tool understands, which is specified as ok to change.
An tool MUST NOT change anything else, including (but not limited to):
- Any values in
metadata. - Any unrecognised standard or custom properties.
- The order of commands/comments within a category.
After an tool outputs a file once, running it back through the same tool SHOULD round-trip, changes are only generally expected when first loading from one into another.