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 maybeimportlib.resources. -
You’re writing some data to a file, and a
HiddenOptionwon’t cut it for some reason (e.g. you’re using sqlite). Pick a folder underSETTINGS_DIR, and put all your files under it - making sure to create the folder if needed. Also set yoursettings_fileto be in this folder inbuild_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:

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:
 -
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.
- https://bl-sdk.github.io/oak-mod-db/requirements?mod=my_mod
- https://bl-sdk.github.io/oak2-mod-db/requirements?mod=my_mod
- https://bl-sdk.github.io/willow1-mod-db/requirements?mod=my_mod
- https://bl-sdk.github.io/willow2-mod-db/requirements?mod=my_mod
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.