Releasing Your Mod

Packaging Your Mod

While working on your mod, you’ll just have a bunch of lose files in a folder. When it comes time to release, we need to package it up more nicely. There are three formats we accept.

You might consider writing a script to package mods automatically. Several contributors have done so; we don’t have a general purpose script since they all have customizations to suit each persons’ needs.

.sdkmod file

The standard format is as a single .sdkmod file, which users just drop directly in their mods folder.

.sdkmod files are really just .zips with a renamed extension. We renammed it mostly to avoid the default Windows file association, which helps prevent users getting confused (extracting files wrong is a very common source of installation issues). The root folder must only contain a single folder, which must be named the same thing as the .sdkmod, and your __init__.py goes inside that inner folder.

Expand for Examples

Ok

my_mod.sdkmod
- my_mod/
  - __init__.py
  - other_file.py

Not ok, missing inner folder

my_mod.sdkmod
- __init__.py

Not ok, too far nested

my_mod.sdkmod
- my_mod/
  - my_mod/
    - __init__.py

Not ok, .sdkmod and folder have different names

My Mod.sdkmod
- my_mod/
  - __init__.py

Not ok, multiple files in root

my_mod.sdkmod
- my_mod/
  - __init__.py
- LICENSE

Not ok, multiple folders in root

my_mod.sdkmod
- my_mod/
  - __init__.py
- dependency_mod/
  - __init__.py

This format is also validated during SDK’s initialization, an incorrectly formatted .sdkmod will not be imported.

Mod Folder Zip

Python is perfectly happy to import files from inside a zip, that’s how .sdkmods work. But occasionally there are reasons you need the user to properly extract your folder (more later). For these cases, the next acceptable format is actually the exact same as a .sdkmod, except you keep the extension as .zip. Our guides all say to extract zips.

There are very few cases you actually need to use this format. The only known cases are:

  • You’re shipping a native Python module (a .pyd) which you need to import.
  • (Willow 2) You’re shipping a text mod alongside your mod, which you want to exec <path> in console.

These are some common use cases which cause people to think they need this format, but which do not, and can work perfectly fine as a .sdkmod:

  • You have some assert file which you need to read from at runtime. Use mods_base.open_in_mod_dir, or in more advanced cases maybe importlib.resources.

  • You’re writing some data to a file, and a HiddenOption won’t cut it for some reason (e.g. you’re using sqlite). Pick a folder under SETTINGS_DIR, and put all your files under it - making sure to create the folder if needed. Also set your settings_file to be in this folder in build_mod(...).

Game Folder Zip

The last format is for hybrid mods, which need both an SDK mod and a upk/pak/some other file in the game folder. These should be a zip file, called .zip, containing all your files arranged relative to the base game folder. Users should be able to install these just by merging all files into their game folder, in the same way you install the SDK.

What we specifically look for in this case is that the root folder inside the zip contains an sdk_mods folder, which itself contains either a single .sdkmod, or a single folder.

Expand for Examples

Ok

my_mod.zip
- sdk_mods/
  - my_mod.sdkmod
- WillowGame/
  - CookedPC/
    - MyMod.upk
- Readme.md       # though maybe don't use a generic name, likely to be overridden
my_mod.zip
- OakGame/
  - Binaries/
    - Win64/
      - Plugins/
        - ohl-mods/
          - my_mod.bl3hotfix
- sdk_mods/
  - my_mod/
    - __init__.py

Not ok, multiple files/folders in sdk_mods

my_mod.zip
- sdk_mods/
  - my_mod.sdkmod
  - Readme.md
my_mod.zip
- sdk_mods/
  - my_mod.sdkmod
  - other_mod.sdkmod
my_mod.zip
- sdk_mods/
  - my_mod/
    - __init__.py
  - other_mod.sdkmod

Not ok, missing sdk_mods folder/not laid out like the game folder

my_mod.zip
- my_mod.sdkmod
- my_mod.pak

Adding to the Mod DB

Both mods_base and the mods db are built around the idea of your mod’s pyproject.toml being a single source of truth. In the past it wasn’t uncommon to update your mod but forget to update the db, or vice versa, which this concept helps avoid. With a well configured pyproject, all your mod details will be extracted automatically.

To add your mod, you’ll need to add a markdown file to one of the _*_mods folders in this site’s repo.

Simplest Configuration

The simplest configuration is the following.

---
pyproject_url: https://raw.githubusercontent.com/apple1417/oak-sdk-mods/master/abcd/pyproject.toml
---

# My cool mod
Does some cool things.

Make sure the url points at an auto-updating link, instead of a specific commit. If you’re using github, also make sure to use the raw.githubusercontent.com link - not the github.com/.../raw/ links, which don’t work.

Please write a detailed description - more so than what you might put in the mod’s in-game description. Historically a lot of mods have had very basic descriptions, which makes it hard for users to find the right thing. If your mod adds a god mode cheat, the words “god mode” should probably be on the page somewhere, so that the searchbar can find it.

More Advanced Markdown

If you want to include images/other assets, add them to a assets/mods/<tree>/<mod_name> folder.

---
pyproject_url: https://raw.githubusercontent.com/apple1417/oak-sdk-mods/master/abcd/pyproject.toml
---

# My cool mod
Look at this image:
![alt text](/assets/mods/oak/abcd/some_image.png)

Jekyll uses kramdown to render markdown, which probably supports some extra syntax than what you’re used to.

There’s also two bits of the templating system you should probably know about:

  • When linking to something that’s part of the site, whether an image or another page, prefer using {{ "/path/to/file.txt" | relative_url }}. This ensures the link will get updated correctly if hosted under another url.

    The above example image should really have been:

    ![alt text]({{ "/assets/mods/oak/abcd/some_image.png" | relative_url }})
    
  • If you want to embed a youtube video, prefer using {% youtube https://www.youtube.com/watch?v=dQw4w9WgXcQ %}.

If you’re using some of these more advanced features, you may want to build the site locally to make sure they display as you expect.

Changing Mod Details

Now you can easily customize the mod description by just writing markdown, but there’s still all the other info above it. You can overwrite these by setting front matter variables. If you’re not familiar with Jekyll, the “front matter” is a block of YAML configuration inbetween triple dashes at the top - you used it previously to set the pyproject_url.

Field Front matter pyproject.toml
Title title tool.sdkmod.name, project.name
Author(s) author project.authors[n].name1
Latest Version version tool.sdkmod.version, project.version
Supported Games2 supported_games tool.sdkmod.supported_games
Coop Support coop_support tool.sdkmod.coop_support
License4 license tool.sdkmod.license, project.license.text5
Requirements dependencies project.dependencies
Misc URLs6 urls project.urls
Download Link download tool.sdkmod.download
Description The page contents project.description7
Redirects8 redirect_from Not supported

1 Multiple authors are concatenated in the order given.
2 An array of strings. If not given, defaults to all games for the category you’re in.
3 One of Unknown, Incompatible, RequiresAllPlayers, or ClientSide. Defaults to unknown.
4 A table with keys name and url. Prefer linking to a summary site, rather than direct to your LICENSE.
5 Used as the name, with no url.
6 A dict where keys are the names and values are the urls.
7 HTML tags are stripped, rather than just being escaped.
8 An array of relative urls to redirect to this page - i.e. if you moved your mod, it’s old urls. See also jekyll-redirect-from.

Again, if you’re using any of these, you may want to build the site locally to make sure they display as you expect.

Updating Mod Details

While this site is statically generated, every time a mod page is loaded it fetches the pyproject and updates the page with any changes, the values fetched when the site is generated are only used as defaults (note that front matter overrides still take priority). This means you generally don’t need to touch the db again, changes will be picked up automatically.

There are a few exceptions to this, which are not automatically updated:

  • The title used in the sidebar and tab title (the header on the mod page does get updated).

  • project.name, which is used for matching dependencies to their mod page.

  • The dependencies displayed on the missing requirements page.

  • The data powering the searchbar.

  • The fields which are always displayed (e.g. Title, Author) will not be set to unknown if you completely delete their section in your pyproject, the old data is preferred.

    Requirements and Misc URLs are already hidden when not in use, so the updates will delete them.

If you make significant changes to your pyproject, it may be worth kicking off another build to update the static versions of these. Do note that this data is updated anytime the site is generated, someone else adding an unrealated mod will update yours.

Handling Missing Requirements

It’s recommended to put dependency version checks right at the top of your main __init__.py, to make sure your mod only loads if everything is present, and to give users more helpful error messages.

One simple way of doing this is using asserts and __import__:

if True:  # avoids E402
    assert __import__("mods_base").__version_info__ >= (1, 4), "Please update the SDK"
    assert __import__("pyunrealsdk").__version_info__ >= (1, 3, 0), "Please update the SDK"
    assert __import__("unrealsdk").__version_info__ >= (1, 3, 0), "Please update the SDK"
    assert __import__("ui_utils").__version_info__ >= (1, 0), "Please update the SDK"

    from mods_base import Game

    assert Game.get_current() == Game.BL3, "The Hunt Tracker only works in BL3"

It’s easiest to set your required versions to be >= the version you’re currently developing with, rather than searching through history to find the oldest possible compatible one.

Now asserts make for nice and simple code, but not all users check console before going out and complaining. As an alternative, you can open a page in their browser instead - this site provides several for that purpose.

try:
    assert __import__("mods_base").__version_info__ >= (1, 5), "Please update the SDK"
except (AssertionError, ImportError) as ex:
    import webbrowser
    webbrowser.open("https://bl-sdk.github.io/willow2-mod-db/requirements?mod=my_mod")
    raise ex

The ?mod= query parameter should be set to your mod’s project.name.

As mentioned above, anything in project.dependencies is shown as a requirement on the mod page. If you want to require a particular version of the SDK, you can add a dependency on oak_mod_manager, oak2_mod_manager, willow1_mod_manager, or willow2_mod_manager (as appropriate).

[project]
dependencies = [
  'oak_mod_manager >= 1.2',
]

Since requiring the SDK is somewhat implicit, it remains to be seen if the possible future package manager will need this.


This site uses Just the Docs, a documentation theme for Jekyll.