Introduction

Mason is a compiler that creates Stratum plugins from a project directory. The resulting DLL fits into the BepInEx ecosystem identically to that of a hand-written DLL; it boasts all the same features as a handwritten BepInEx plugin, such as dependencies, incompatibilities, and process name restrictions, with much less of a hassle to setup and maintain.

The Book of Masonry is the documentation for Mason, and intends to detail every feature in a human-readable form. Some of these features are located in the project and config files, whose schemas you can find here.

Contributions to the book are always welcome, even ones as small as correcting a typo or simplifying a confusing sentence. You can make a suggestion directly to a contributor (such as Ash), create an issue on the repository, or click the notepad in the top right corner of the chapter to edit a file and make a pull request.

As A Book

As this documentation site most closely resembles a book, a few expectations should be established.

Terminology used in books has been adopted. Individual web pages, such as this introduction page, are referred to as chapters. Within chapters, headings denote sections. A group of similar chapters comprises a unit. These distinctions were made to avoid confusion between "section" in the sense of a heading and "section" in the sense of a chapter.

To avoid repetitive content, later chapters assume you have read the former chapters. If a former chapter references content in a later chapter, it will be noted.

Disclaimers

This book contains many examples of Mason project file features. To keep things concise, only relevant lines are displayed. Other information, most notably the version: 1 at the beginning, may be required but not shown.

Getting Started

The Getting Started unit is a pragmatic approach to learning the basics of Mason. It details, start to finish, how to create a Thunderstore package using Mason.
The chapters consist of:

Installation

Before Mason can be used, it first needs to be installed.

Mason can be found on Thunderstore or GitHub. It is recommended and assumed that you are using r2modman or Thunderstore Mod Manager, as the Stratum ecosystem leverages the Thunderstore ecosystem extensively.
It is recommended to create a new profile for the sole purpose of running Mason. This keeps start times slim and lowers the chances of a mod conflict while you are still developing your mod, but always remember to test your mod for conflicts later!

Preparation

To prepare a Mason project, first navigate to where your mods are being installed. For r2modman or Thunderstore Mod Manager users, this can be found in Settings > Locations > Browse profile folder.
Once you have arrived at the folder, navigate to BepInEx/plugins (you may have to run the game once for this folder to generate). Create a new directory, which will be the root of your Mason project. Your project path should look something like BepInEx/plugins/wip-mod

Populating the Project

Mason projects require some files before they can be compiled. Each of the following sections describe paths within your Mason project.

Project File

The project file contains the instructions that Mason should convert into runnable code. The project file is located at project.yaml, and must contain

version: 1

at the start.

Wow! What an amazing project! In future examples, this will be omitted. However, it is critical that you include the version field in every project file. Failure to do so will result in the project not compiling.

Thunderstore Manifest

The Thunderstore manifest contains the metadata that Thunderstore requires in every package. To avoid redundancy, Mason reads from this manifest to create metadata for your plugin. The following fields are the bare minimum to compile a Mason project, and are not representative of your final manifest:

{
  "author": "AuthorName",
  "name": "ModName",
  "version_number": "1.0.0",
  "description": "This does something awesome, I only have 250 characters to explain!"
}

The author field is not part of the Thunderstore specification, however it is greatly recommended to include it. If not present, Mason will perform the following checks:

  1. Does the project directory name have the pattern AuthorName-ModName?
  2. Does the ModName of the directory match the ModName of the manifest?

If the checks pass, it will use the AuthorName from the project directory.

The author field must be a valid Thunderstore team name (same restrictions as a package name), so an author that does not match this will cause a failure.

Compilation

Compilation turns a Mason project into something runnable. We haven't added any assets yet, but that is OK. Mason can compile an empty Stratum plugin, and that's all you need to learn on this page.

How to Compile

To compile your project, simply launch the game modded. BepInEx will trigger Mason, which will then sift through all projects and compile them. The resulting project directory should look something like:

MyProject
 ├── bootstrap.dll
 ├── config.yaml
 ├── manifest.json
 └── project.yaml

If not, something probably went wrong. Check your console/log for any errors from Mason. They may be global, thus preventing Mason from compiling any projects. They may also be specific to your project, in which case they would detail what you did wrong.

What Each File Is

Mason produces two files as a result of compilation: bootstrap.dll and config.yaml. These files serve two very distinct purposes.

bootstrap.dll

bootstrap.dll, or the bootstrap file, is the code that gets run by BepInEx. This is the living embodiment of your project, and what makes it work. The bootstrap file does not require any of the other files present in order to function.

config.yaml

config.yaml is a special file intended for use by standalone Mason installations. It is detailed further in the standalone unit, but for now it can be ignored.

Running the Result

If you have no errors, the project will run itself! The bootstrap file will be naturally discovered and executed by BepInEx, resulting in the project being ran. Currently, it has no assets, so nothing is performed.

Assets

Stratum plugins without assets, much like Thunderstore packages, are rarely useful. They're like a waterbottle without the water. They are also the most complex part of Stratum plugins.

This page is very YAML-heavy. If these files look alien to you, this website has a good breakdown of YAML syntax. It is very similar to JSON, just more terse and packs more features.

Adding Assets

First, create a resources folder within your project. This is where all your resources will reside.

One Asset

Assuming we have a gun within the asset bundle my_gun that we would like OtherLoader to load instantly, simply place the asset bundle in the resources folder. Using this simple project.yaml would cause it to load:

assets:
  runtime:
    assets:
      - path: my_gun
        plugin: h3vr.otherloader
        loader: item

In this example, the first assets node declares the assets of the entire plugin. The second assets node narrows it down to just the assets of the runtime stage.
The runtime assets node takes a sequence of assets, which is show above after the hyphen. path is the path to the file, relative to the resources folder. plugin is the BepInEx GUID of the plugin that contains the loader, and loader is the name of the loader. Together, these three data make a single asset.

Multiple Assets

Say you are using OtherLoader's new on-demand loading feature. This requires two assets: one for the data asset bundle, and one for the late asset bundle. This example will use my_gun and late_my_gun correspondingly:

assets:
  runtime:
    sequential: true
    assets:
    - path: my_gun
      plugin: h3vr.otherloader
      loader: item_data
    - path: late_my_gun
      plugin: h3vr.otherloader
      loader: item_first_late

The most notable difference in this example is the sequential tag. By default, runtime assets are ran in parallel. sequential: true specifies that it should load in series, causing them to be loaded one at a time. If it was not specified, the late asset bundle may load before the data asset bundle!

Multiple Assets in Distinct Stages

Again with OtherLoader's new on-demand loading feature: if you have multiple asset bundles for the data or late loader, you must use nested pipelines. A pipeline is just a collection of subpipelines and assets to run. In fact, the runtime node is actually a pipeline.
The necessary project file looks slightly different, but reads approximately the same:

assets:
  runtime:
    sequential: true
    nested:
    - assets:
      - path: data/*
        plugin: h3vr.otherloader
        loader: item_data
    - assets:
      - path: late/*
        plugin: h3vr.otherloader
        loader: item_first_late

This project file is intended for use with such a resources folder as:

resources
 ├── data
 │   ├── first_gun
 │   └── second_gun
 └── late
     ├── late_first_gun
     └── late_second_gun

This is also the most detailed example, so it may be helpful to see it in the context of the rest of the project file. The following example is a complete project file, contrary to the warning at the beginning of the book:

version: 1
dependencies:
  hard:
    h3vr.otherloader: 1.0.0
assets:
  runtime:
    sequential: true
    nested:
    - assets:
      - path: data/*
        plugin: h3vr.otherloader
        loader: item_data
    - assets:
      - path: late/*
        plugin: h3vr.otherloader
        loader: item_first_late

For further explanation of assets, see the assets unit. This page specifically discusses the runtime stage

Dependencies

While the project does run and have assets, its lack of declared depenendencies makes it vulnerable to a dependency-not-found error. Adding dependencies to your project ensures all dependencies are present and loaded before your plugin runs.

To add a dependency to the project, use the dependency mapping. Another mapping, hard, is needed to specify that it is an absolute requirement. Within this mapping, the globally unique identifier (GUID) of the plugin is used as the key, and the minimum version required is the value. For OtherLoader, dependencies in the project file would look like:

dependencies:
  hard:
    h3vr.otherloader: 1.0.0

h3vr.otherloader because OtherLoader's BepInEx GUID is such. Most loaders will have their GUID listed in their documentation, Thunderstore page, or GitHub README.


For more information on dependencies, see their chapter.

Packaging

Now that the mod is complete, it can be packaged! Be sure to run Mason before you package to ensure the bootstrap file is fully up to date with your project file and resources, and finish up any Thunderstore-required files.

Packaging a Stratum plugin is a little more complicated than a Deli mod, but it's just as easy. Our current project directory is as follows:

MyProject
 ├── resources
 │   └── my_gun
 ├── bootstrap.dll
 ├── config.yaml
 ├── icon.png
 ├── manifest.json
 ├── project.yaml
 └── README.md

The Thunderstore-required files must stay where they are, so we can add them like usual. Similarly, the bootstrap file is in the correct location for the package.
The config and project files can be ignored, as they are only used to generate the bootstrap file. They may also be included in the package, but serve no meaningful purpose.
This leaves only the resources folder. Because we require the resources folder to not be flattened, we must follow r2modman's style of packaging and place the resources folder within a plugin folder.

With all of these factors considered, the following should be what your mod structure looks like:

MyMod
 ├── plugins
 │   └── resources
 │       └── my_gun
 ├── bootstrap.dll
 ├── icon.png
 ├── manifest.json
 └── README.md

Zip the contents of the folder and it's ready to upload!

Converting Deli Mods

As Mason + Stratum is the replacement for Deli, creators should convert their Deli mods to Stratum plugins when they find it is possible. Conversion is performed automatically through Deliter (/dɪˈliːtər/), which converts all Deli mods possible within the current profile. Mods may be converted by hand, but Deliter is faster.

Installing Deliter

Deliter can be be found on Thunderstore or GitHub.

Converting

Install all the mods you wish to convert, then launch the game. Deliter will convert all it can to Stratum plugins, even if it cannot convert the entire mod. Once the preloader finishes, it is done.
The directory of a delited mod should look similar to:

SomeMod
 ├── resources
 ├── AMod.deli.bak
 ├── bootstrap.dll
 ├── config.yaml
 ├── icon.png
 ├── manifest.json
 ├── project.yaml
 └── README.md

AMod.deli.bak is just a copy of the original mod file, in case the original mod is not available on Thunderstore for whatever reason.
The original file, in this example AMod.deli, may actually still exist under some circumstances. This happens when a mod cannot be fully converted, but is instead partially or not at all converted. To check if the mod was converted at all, simply view if there are any assets in the project file. No assets means no conversion. In these scenarios, mods still require Deli.

Repackaging

As Deliter simply generates a Mason project, the steps listed on the packaging page of Getting Started can be followed with a few additions.

The most important thing to do when repackaging a delited mod is: do not include the .bak file. This is the original Deli mod, which is already on Thunderstore within an older package version. It serves no functional purpose and can be omitted.

Partial conversions also need to be cared for more than complete conversions. You can package the partially converted Deli mod with no edits, but that could as much as double the size of the package.
If possible, skim through the manifest file of the Deli mod and check what files/folders are still used. Deliter is not smart enough to delete them, so you must do this manually.
The same goes for the resources folder; using the project file as a guide, trim any excess resources. This will result in a smaller Thunderstore package. For more advanced users, the standalone section details automatic packaging which will do this process for you.

Problem System

The problem system of Mason, which manifests itself as errors and warnings, exists to inform creators of problems that exist in their projects before they are ran. This increases the speed at which projects can be worked on, because you no longer need to wait for the game to launch before seeing if there are any problems. This system also integrates well with Visual Studio: Code, to the degree that you might not notice how it works, as discussed on that page.

Consider this warning:

warning at manifest.json(0,0): [C10] The author of the mod was infered by the directory name. Consider adding an 'author' property.

The Message

The most important element of this problem is the message:

The author of the mod was infered by the directory name. Consider adding an 'author' property.

The message is often what you will see first, and is fairly self explanatory. It explains the problem, and attempts to give instructions on how to resolve it.

Message Identifiers

Each message format (the message before it has any specific data inserted) has its own identifier, in this case C10.
The character indicates which component of Mason emitted the problem, and the number is the unique ID of the message format within that component of Mason. The possible components are:

  • C: compiler (used by patcher and standalone)
  • S: standalone
  • P: patcher

Severity

The next datum of note is the severity. If you have any experience reading logs, this should be a breeze. Mason contains only two severities: warning and error. Warnings represent a potential future flaw, or that something can be done easier. Errors represent an issue in the project that causes it to be uncompilable.

Location

The last piece of information in the probem is its location. With this problem, it is manifest.json(0,0). Locations come in four kinds, with each being more specific than the last:

Global

Global errors are the rarest and are rarely due to the project itself. They are the every-day error of Mason, and are shown as .(0,0).

File and Folder

File/folder specific errors are due to problems with the file's presense, location, or holistic contents. They are shown as path/to/item(0,0). This example is a holistic error, as there is no specific point in the file that should have the author property, but the file as a whole should contain it.

File Range

File range errors are caused by a segment of text within the project. They take the form of path/to/item(X1,Y1-X2,Y2), where X1 is the beginning line, Y1 is the beginning column, X2 is the ending line, and Y2 is the ending column.

File Point

File point errors are mainly errors in the parsing libraries that Mason uses (code that reads JSON/YAML), which reports not ranges of text but points. They take the form of path/to/item(X,Y) where X is the line number and Y is the column number.

Metadata

Mason makes available the rich metadata options of BepInEx plugins. Currently, the metadata available are:

Finding the GUID

Metadata that reference other plugins, such as dependencies and incompatibilities, require the GUID of other plugins. If you are not directly told the GUID of a plugin, how are you supposed to find it?

Plugins compiled using Mason always have a GUID with the format of Author-Name. This is almost always identical to the dependency string on Thunderstore, but without the version. For example, nayr31-VeprHunter-1.0.0 would be nayr31-VeprHunter. The only instances where this is not the case is when the team name (uploader) of the package is not the same as the author field of the project's manifest, and is usually caused by reuploads and project forks.
Consider a project, MedicBag, which is originally created by Dallas. Dallas uses Dallas as his Thunderstore team name and author field. The Thunderstore dependency string would be Dallas-MedicBag-x.y.z and the GUID would be Dallas-MedicBag. If Hoxton uploaded his edit of the project, the project would have the dependency string Hoxton-MedicBag-x.y.z. Given that Hoxton did not change the author field and recompile the project, the GUID of his mod would remain Dallas-MedicBag.

Plugins created from scratch (i.e. not using Mason) can have whatever they wish as their GUID. There are few restrictions on the format, and you will need the plugin creator to specify what the GUID is. They may provide documentation on the Thunderstore page, GitHub README, or other means.

Dependencies

Dependencies are a metadatum that instructs BepInEx on what plugins should load before your plugin, and asserts the version of the other plugins.
They come in two forms: hard and soft.

Hard Dependencies

Hard dependencies are the most common form of dependency and probably what you think of when you hear the word "dependency." A hard dependency requires another plugin to be present, and requires that it is at least a certain version. Given that these criteria are satisfied, your plugin will load after the other plugin. In 99% of cases (source: me), a hard dependency is what you need.

Hard Example

dependencies:
  hard:
    h3vr.otherloader: 1.0.0

This requires that OtherLoader 1.0.0 is present, and causes it to load before our plugin.

Soft Dependencies

Soft dependencies are the long lost brothers of hard dependencies. A soft dependency checks if a plugin is present, and if so, causes it to load before your plugin, but does not cause your plugin to fail if it is not present. Soft dependencies have niche use cases, so much so that I cannot think of a reason to use one in the context of Mason.

Soft Example

dependencies:
  soft:
  - wristimate

If Wristimate is present, the plugin will load after it. I don't know why you would want to do this, but I can't think of any good examples.

Dependency Tips

Good dependencies make a good plugin. Always depend on the the lowest possible version that has your loaders. This allows users to revert the loader plugin for whatever purpose is necessary, most notably it breaking.
Also, use as few dependencies as possible. The more dependencies, the more points of breakage in a plugin.

Incompatibilities

Incompatibilities are a metadatum much like the opposite of a hard dependency: they prevent your plugin from loading if the specified plugin is present. You may want to do this if a plugin obsoletes another plugin, and you want the user to notice and uninstall the old version.

Example

incompatibilities:
- MyAuthor-OlderModName

This example prevents the plugin from loading if MyAuthor-OlderModName is present.

Processes

Processes are a metadatum that only allows the plugin to be loaded in certain games/applications. You should only use this if your loaders are available for multiple games. Given that the loaders are only available for one game and you have listed the loaders as dependencies, it follows that your plugin will only ever run on that one game.

Example

processes:
- h3vr.exe

This prevents the plugin from running on any game except for H3VR.

Asset Management

Assets are the most important element of nearly all Mason projects. A strong understanding of assets shows a fairly good understanding of Mason.

Lexicon

Before we continue, some definitions should be established:

A loader is a plugin that features some code to convert data into some effect in game. An asset is one or more resources, which are files and folders used by an asset, that are passed to a loader at a certain time. A stage is a Stratum process that executes every plugin, thus every asset.
For example, OtherLoader is a loader that turns asset bundles into spawnable guns in game. VeprHunter contains an asset bundle (which has the data of the gun), which then gets passed to OtherLoader. The process of passing the asset bundle to OtherLoader is the asset, but the asset bundle itself is a resource because it is used in an asset. This entire interaction is facilitated by the runtime stage, which runs every plugin's runtime callback (thus running every plugin's runtime assets) while the game is running.

While assets and resources are closely related, it is important to note the difference so the contents of a project can be clearly and concisely explained.

The Asset Definition

The remaining chapters of this unit cover much about assets, but not do not define them. Instead, assets an asset definition will be shown and dissected in this very section.

Yet another term to learn! Asset definition is simply the text within a project file that represents an asset; it is the way an asset is defined. Assets have the structure of:

path: gun_bundle
plugin: h3vr.otherloader
loader: item

but the properties can be reordered, of course.
An asset definition can be broken into the following components:

  • path: the path to the resource, relative to the resources folder
  • plugin: the BepInEx GUID of the plugin that contains the loader
  • loader: the name of the loader

The Setup Stage

Setup assets follow the WYSIWYG principle. They have no quirks and use a format extremely similar to Deli, in which assets are placed in a sequence:

assets:
  setup:
  - path: readme.txt
    plugin: motds
    loader: init
  - path: readme confirm.txt
    plugin: motds
    loader: init

In this case, the readme.txt asset runs to completion before the readme confirm.txt asset begins. When the second asset finishes, the plugin's setup callback is complete.

The Runtime Stage

The runtime stage, being that it is not as restricted as the setup stage, has significantly more features and complexity.

Starting from Assets

The runtime and setup stage, although very different in abilities, both share the purpose of running assets.

Assuming the loaders are available for the runtime stage as well, the same asset sequence can be used with the runtime stage:

assets:
  runtime:
    assets:
    - path: readme.txt
      plugin: motds
      loader: init
    - path: readme confirm.txt
      plugin: motds
      loader: init

However, may produce a different result than the setup stage because of the sequentiality, which is the way the assets are executed. Setup is constrained into one sequentiality, hence it being just a list. Runtime contains two possible sequentialities, which are discussed in the first chapter of this unit. Runtime also contains other features, such as nested pipelines, which are possible because of sequentiality.

Sequentiality

Sequentiality is the way assets are executed. Using the wrong sequentiality for the job can result in bugs traditionally called race conditions.

If sequentiality allows creators to screw over their own projects, why bother with the feature at all? Consider this runtime stage:

assets:
  runtime:
    assets:
    - path: guns/pistol
      plugin: h3vr.otherloader
      loader: item
    - path: guns/rifle
      plugin: h3vr.otherloader
      loader: item
    - path: guns/smg
      plugin: h3vr.otherloader
      loader: item

Because the default sequentiality is parallel, this runtime stage is running the assets in parallel. With parallel sequentiality, the plugin does not await assets to begin the next one. It begins guns/pistol, guns/rifle, and then guns/smg before any of the previous assets have completed. The assets may finish in the same order, but it is never certain. This may result in bugs if creators do not factor in parallel processing.
Although it can cause problems, it pays dividends in accelerating the loading time. Each asset bundle is not dependent on any of the other asset bundles, so all asset bundles are able to load independently and thus simultaneously. These gun bundles may be massive, requiring solid chunks of time to decompress on a single thread. In series, these all compete for the same hardware resources and thus the total time to load is the sum of each individual load. When adjusted to be ran in parallel, though, the total loading time of the plugin is greatly reduced.

Overriding Sequentiality

If the default, parallel sequentiality does not suit your needs, you can override it with the sequential property. This is especially useful for OtherLoader's on-demand loading feature:

assets:
  runtime:
    sequential: true
    assets:
    - path: pistol
      plugin: h3vr.otherloader
      loader: item_data
    - path: late_pistol
      plugin: h3vr.otherloader
      loader: item_late

In this example, pistol begins and finishes loading before late_pistol begins loading. This prevents the late asset bundle, which should be loaded last, from being loaded earlier. To load the other guns in parallel in the same fashion, we need nested pipelines.

Nested Pipelines

We've covered sequentiality, which allows loading individual assets before others, but what if you wish to load entire batches of assets before others? That's where you need nested pipelines.

Nested pipelines have the same syntax as the runtime stage. In fact, the runtime stage is a pipeline! Nested pipelines are just a list of pipelines that are executed before the assets, in the same fashion as the assets. For example:

assets:
  runtime:
    sequential: true
    nested:
    - assets:
      - path: rifle
        plugin: h3vr.otherloader
        loader: item_data
      - path: pistol
        plugin: h3vr.otherloader
        loader: item_data
    - assets:
      - path: late_rifle
        plugin: h3vr.otherloader
        loader: item_late
      - path: pistol
        plugin: h3vr.otherloader
        loader: item_late

In this example, the nested pipelines act like substages; all the assets of the first pipeline finish loading before any of the assets of the second pipeline begin.

Sometimes, you have many assets that can be loaded in parallel but some assets that must be loaded in series. Nested pipelines allow these two to be combined into one project:

assets:
  runtime:
    nested:
    - sequential: true
      assets:
      - path: readme.txt
        plugin: motds
        loader: init
      - path: readme confirm.txt
        plugin: motds
        loader: init
    assets:
    - path: rifle
      plugin: h3vr.otherloader
      loader: item
    - path: pistol
      plugin: h3vr.otherloader
      loader: item

This example begins loading both guns at the same time, as well as the nested pipeline. The nested pipeline loads both readme files, but does so one at a time.

Globbing

Globbing, or the use of wildcards, is a feature you've likely used many times already. It allows multiple things, in this case files and folders, to be specified by one string. Currently, globbing is only enabled for the path property of an asset definition:

assets:
  runtime:
    assets:
    - path: guns/*
      plugin: h3vr.otherloader
      loader: item

Any time a path separator (/) is used, the result of the glob is filtered to only the directories of the previous glob.

A Runtime Forewarning

Globbing can be another catalyst of runtime bugs. Consider:

assets:
  runtime:
    sequential: true
    assets:
    - path: guns/data/*
      plugin: h3vr.otherloader
      loader: item_data
    - path: guns/late/*
      plugin: h3vr.otherloader
      loader: item_late

This project file appears to load all data asset bundles parallel, then late asset bundles in parallel. However, all a glob does is expand; an asset definition with a glob in a list is converted into many assets in the same list. This project would load all data asset bundles and late asset bundles in series.

To resolve this problem, simply create a nested pipeline for each asset definition:

assets:
  runtime:
    sequential: true
    nested:
    - assets:
      - path: guns/data/*
        plugin: h3vr.otherloader
        loader: item_data
    - assets:
      - path: guns/late/*
        plugin: h3vr.otherloader
        loader: item_late

In this case, the globs will expand within the nested pipelines. This would load all data asset bundles in parallel, wait for all to complete, then begin to load all late asset bundles.

Glob Types

Mason supports many globs, which may be used to your delight.

Name

Name globs are globs that match files or folders within the directory, and may be composited together within one path segment (text between path separators). Literals (plain text) also fall under this category.
A composite name glob may be [cb]at*, which would match cat, bat, batmobile, cathode tube and more, but not rat, brat, latissimus, and many others.

Some name globs do not require any data to fulfill their purpose:

GlobPurpose
*Matches 0 or more characters
?Matches exactly 1 character

while others do:

GlobPurposeDoes MatchDoes Not Match
[bc]Matches any character within the bracketsb, ca, d .. z, A .. Z, 0 .. 9 and more
[!bc]Matches any character except those within the bracketsa, d .. z, A .. Z, 0 .. 9 and moreb, c
[0-5]Matches any character between the two characters0 .. 56 .. 9, a .. z, A .. Z and more
[!0-5]Matches any character except those between the two characters6 .. 9, a .. z, A .. Z and more0 .. 5

Globstar (**)

Globstar matches every file and empty folder within the current folder and any subfolders, as well as the current folder itself. This is often used to glob any file within a folder with a certain name, regardless of which directory it is in. As an example, gun/**/data/* would match gun/data/something, gun/blue/data/anything, gun/green/chartreuse/data/you name it

Escape Characters

Globs can be escaped, causing the glob to become a literal. This can be done by putting a backslash (\) before the brackets. For example, [brackets].txt will only match b.txt, r.txt, a.txt, and so on, but \[brackets\].txt will only match the file named [brackets].txt.
Be careful with escaping both brackets, as \[brackets].txt will merely prefix the glob with a backslash. Similarly, [brackets\].txt does not escape the glob, but adds backslash as a possible character that can be matched. As a backslash cannot be used in a file/folder name on Windows, both of the errorful globs will match nothing.

Standalone

Up until this point, only the patcher plugin has been used. While the patcher plugin is very easy to begin with, it leaves much to be desired in relation to its workflow. Needing to launch the game to compile a mod, reading the console for warnings/errors, and manually packaging are possible, but tedious actions.
The standalone version of Mason aims to streamline the workflow as much as possible, and is even cross-platform!

Installation

Mason can be installed as a NuGet tool or by downloading the EXE directly.

NuGet

NuGet is the easiest, but is only available if you have the .NET SDK installed. If you don't know what that is, proceed to the direct download. For those with the SDK, use

dotnet tool install --global mason

to install and

dotnet tool update --global mason

to update.

Direct Download

Direct download, while primitive, works without any additional programs. Visit the latest release and download the standalone zip for your OS. Extract this to a directory, then add it to your PATH (for Windows). Adding to PATH ensures Mason can be called from anywhere on your PC.

To update Mason, extract a newer release to the same folder as before. Delete all items before extracting to ensure there are no unused files.

Usage

Before calling standalone Mason, you must add a config.yaml file to your project directory. This file instructs Mason on where it can find the DLLs it needs, as it does not know where the game files are like the patcher plugin does. This file may appear as:

directories:
  bepinex: C:\Users\<user>\AppData\Roaming\Local\r2modmanPlus-local\profiles\H3VR\<profile>\BepInEx
  managed: C:\Program Files (x86)\Steam\steamapps\common\H3VR\h3vr_Data\Managed

Additionally, this BepInEx directory must have Stratum installed.

Command Line

Invoking Mason from command line is as simple as invoking:

mason

from within a project directory.

Mason contains a help page, which like many other shell utilities, can be accessed via a --help flag:

$ mason --help
mason 1.0.4
Copyright (C) 2021 Stratum Team

  build      Builds the mod, which outputs the plugin file.

  pack       Builds and zips the mod, which outputs the mod ready for upload to
             Thunderstore.

  help       Display more information on a specific command.

  version    Display version information.

Visual Studio: Code

Mason integrates fairly nicely with Visual Studio: Code. With the full suite of options, Code offers manifest.json and project.yaml IntelliSense (auto-completion with descriptions and examples), build and pack tasks, and errors appear directly within the text files. The necessary files can be found in the quickstart repository, but you might as well just download the entire repository if you choose to use the files.

Tasks are organized as build tasks, meaning CTRL + SHIFT + B (by default) prompts you with a list of options. Running one of them will either succeed, or bring focus to the problems panel. Problems will be underlined when possible in files, and Mason must be re-ran to update its reported problems.

Features

Standalone Mason, being that it is its own application, has some added features compared to the patcher plugin.

Automatic Packaging

As covered in the Getting Started article, packaging can be a bit tedious because of all the factors to consider, compared to Deli's simple "zip everything in the folder" doctrine. Also, it can be easy to forget to run Mason before packaging, causing you to package an old bootstrap file. Automatic packaging allows you to put in the same amount of effort as you did with Deli, but yield a more efficient result.

The pack command, mason pack, first begins compilation. When Mason compiles a project, it must enumerate through each asset definition to execute the path glob. Mason stores the matching files/folders in a buffer. For example, a project with this structure:

MyProject
 ├── resources
 │   ├── data
 │   │   ├── first_gun
 │   │   └── second_gun
 │   ├── banking info
 │   │   ├── birthday.txt
 │   │   ├── credit card.txt
 │   │   ├── mother's maiden name.txt
 │   │   └── social security number.txt
 │   └── late
 │       ├── late_first_gun
 │       └── late_second_gun
 ├── bootstrap.dll
 ├── config.yaml
 ├── icon.png
 ├── manifest.json
 ├── project.yaml
 └── README.md

and these assets:

assets:
  runtime:
    sequential: true
    nested:
      - assets:
        - path: data/*
          plugin: h3vr.otherloader
          loader: item_data
      - assets:
        - path: late/*
          plugin: h3vr.otherloader
          loader: item_first_late

will only use the following resources:

 .
 ├── data
 │   ├── first_gun
 │   └── second_gun
 └── late
     ├── late_first_gun
     └── late_second_gun

The stored paths are then used to determine which files are added to a Thunderstore package. The result of this example would be:

 .
 ├── plugins
 │   ├── resources
 │   │   ├── data
 │   │   │   ├── first_gun
 │   │   │   └── second_gun
 │   │   └── late
 │   │       ├── late_first_gun
 │   │       └── late_second_gun
 │   └── bootstrap.dll
 ├── icon.png
 ├── manifest.json
 └── README.md

As you can see, only the necessary files are packaged. It also places the resources folder within the plugins folder for you. All from running one command!

Version Control with Git

Version control is essential to any long-lasting project. As the name implies, it manages versions of the project files. This allows an entire project to be reverted to an earlier point in time.

Git is by far the most popular version control system. Git may be initialized within a Mason project like any other directory. Git also has the added benefit of being easily hostable on websites such as GitHub, allowing for community collaboration and additional documentation for your mod.

This book will only cover Git features specific to Mason, not how to use it. There are already mountains of information on the subject that have been constructed much better than anything possible here.

Continuous Integration

Continuous integration is a feature of many source control websites that, as the name implies, continuously integrates source code changes into the product. For the purposes of Mason, continuous integration is just automatic packaging.

GitHub

Continuous integration with GitHub is simple. Simply copy this file into your project, using the same path. GitHub will run this script every time a push or pull request is made to the main or master branches that affect the contents of a Thunderstore package, and it will yield an artifact containing the Thunderstore package. Additionally, you may add a branch protection rule to the main branch and require that the script succeeds before a pull request can be merged.