Create / Publish a mod
Publish a mod
This guide starts after your mod already works locally. It prepares the release, runs the publishing checks, and generates the static portal files that players install.
If you still need the folder or manifest, start with Mod files. For content authoring, use Create your first scenario, Campaign files, or Ship sections for mods.
1. Prepare the release folder
Portal source mods live at webmods/<id>/. The directory name is the public mod
id and URL segment.
Create this directory and file layout manually:
webmods/my-mod/
|- my-mod.bundle.ron
|- my-mod.content.ron
|- CHANGELOG.md
`- thumbnails/
`- mission.png
Use only lowercase ASCII letters, digits, and - for the directory id. Do not
reuse a shipped mod id.
Create webmods/my-mod/my-mod.bundle.ron and write:
(
content: ["my-mod.content.ron"],
resources: ["thumbnails/mission.png"],
meta: (
name: "My Mod",
description: "One complete mission.",
author: "Your Name",
version: "1.0.0",
dependencies: ["base"],
),
)
Create webmods/my-mod/my-mod.content.ron. Put the tested Scenario((...))
item from your local mod inside its content list:
[
Scenario((
id: "my_mod_first_mission",
name: "My First Mission",
description: "One complete mission.",
cubemap: "dep://base/textures/cubemap.png",
events: [
// Use the handlers you already tested.
],
)),
]
Copy any images, models, or sounds that the scenario uses into this directory
and list each path under resources. Keep your own ids unique; prefix them with
my_mod_. The bundle must contain exactly one root *.bundle.ron file, and
every listed content file, resource, and dependency must exist.
base is always available, so declaring it is optional. The portal mods declare
it for clarity.
2. Set the version
The version is an opaque release string. The game detects an update when the installed version differs from the catalog version.
Use a clear release sequence such as 1.0.0, 1.1.0, and 1.1.1. Change the
version before every republish. The portal stores files under
<id>/<version>/; publishing changed files under an unchanged version makes the
update indistinguishable from the previous release.
Keep a mod-local CHANGELOG.md when the mod has more than one release:
# Changelog
## 1.1.0
- Added a second mission.
- Rebalanced the escort ship.
3. Lint the mod
From the repository root:
nix develop --command cargo run content lint \
--target webmods/my-mod \
--report /tmp/my-mod-report.html
Open /tmp/my-mod-report.html. Fix every error and review every warning. The
report checks references, duplicate ids, asset membership, ship geometry,
scenario flow, and other content rules.
A fairness warning you INTEND belongs in the mod's own
balance_acks.ron. The linter reads
that file from the bundle it lints, so the reason travels with the mod.
4. Generate the portal
Generate a local portal tree:
python3 scripts/gen-portal.py \
--source webmods \
--shipped assets/mods.catalog.ron \
--out /tmp/nova-mod-portal
The command validates every portal mod and exits non-zero on a publishing error. It writes:
/tmp/nova-mod-portal/
|- catalog.json
`- my-mod/
`- 1.0.0/
|- my-mod.bundle.ron
|- my-mod.content.ron
`- thumbnails/mission.png
Each catalog.json file entry includes its size and SHA-256 hash. The game
checks both while downloading.
Open /tmp/nova-mod-portal/catalog.json and confirm your id, name, version,
dependencies, and file list.
5. Publish
Add the complete webmods/my-mod/ folder to the repository and land it on the
published branch. The next web deployment regenerates the portal catalog and
copies the versioned files.
Players then:
- Open Mods.
- Browse the online catalog.
- Install the mod.
- Enable it.
- Open Scenarios to play its visible scenarios.
Downloaded files are verified before installation. The mod loads through the same bundle and content pipeline used by shipped mods.
Publish an update
For every update:
- Change the content or resources.
- Bump
meta.version. - Add a matching changelog entry.
- Run content lint and open its report.
- Regenerate
/tmp/nova-mod-portal. - Confirm the new version in
catalog.json. - Publish the changed mod folder.
Changing dependencies is a content change. Use a new version and mention the change because enabling the update can add or remove another mod's overlays.
Preview the repository portal
The deployed portal is static. The site serves the game at /play/, the
catalog at /mods/catalog.json, and versioned mod files under
/mods/<id>/<version>/. The browser requires the game and portal to use the
same origin.
For a live repository preview that regenerates the portal when webmods/
changes:
nix develop --command scripts/serve-web.sh
Open the URL printed by the script. Launch the web game, open Mods, and use Explore online. The site, game, and portal use separate helper servers but are proxied onto one origin.
For the closest check to deployment, build and serve the complete static site:
nix develop --command scripts/preview-web.sh
This creates the production /play/ and /mods/ sibling layout. It does not
watch files; rerun it after a change.
Native builds do not enforce browser CORS. To test the generated portal against a native game:
nix develop --command scripts/serve-mods.sh
# Use the printed port in a second shell:
NOVA_MODDING_PORTAL_URL=http://localhost:<port>/mods \
nix develop --command cargo run
Do not place the portal under /play/mods/. Web builds always look for the
/mods/ sibling.
What the portal verifies
The generator is a package gate. It checks bundle metadata, file membership, ids, dependencies, resource references, sizes, and hashes. It does not prove that scenario and section data can load or play correctly. This is why a release requires both checks:
content lintfor loading and content rules.gen-portal.pyfor package and catalog rules.
The repository's CI runs the deeper loader integration coverage. Mod authors do not need a separate local load-test command after lint succeeds.
The generated catalog is deterministic. Each entry contains the mod id, version, manifest path, metadata, total size, and every file's size and SHA-256 hash. The game verifies downloaded files before committing them to its local mod cache.
Publishing failures
- No mod found -
--sourcepoints at the wrong directory. - Invalid id - rename the folder to lowercase letters, digits, and
-. - Wrong bundle count - keep exactly one root
*.bundle.ronfile. - Missing member - a
contentorresourcespath does not exist inside the mod folder. - Unknown dependency - publish the dependency too or use a shipped id.
- Shipped id collision - choose a new portal id.
- No visible scenario - leave
hiddenunset on at least one scenario that players should launch directly.