Starship Horizons
Log In
☰
Knowledge Base›Packaging Modules

Packaging Modules

A module can ship as a folder of loose files, or as a single .hyd package. Packaging is what you do when a module is finished and you want to hand it to someone.

Why package

  • One file. A module is thousands of files — the base game module is around 2,800 entries. Distributing that as a folder is awkward and easy to get wrong.
  • Faster startup. This is the strongest practical argument. The package stores its contents in boot order, so starting up is one sequential read through the front of the file instead of thousands of scattered ones.
  • Integrity. A signed package can be checked as unmodified since it was signed, and it carries a fingerprint you can publish so others can tell it is yours.
  • A higher floor. "Open the folder and copy the artwork" becomes "understand an undocumented container".

Building one

In the module editor, the Package section under Export shows the module, its version, the file that will be written, and whether signing is configured. Press Build Package.

The output is named from the module id and version:

Packages/DSC-1.0.1.hyd

The version comes from the version attribute on module.xml's root element, read as major.minor.build. Version is part of the filename, so successive builds sit alongside each other rather than overwriting. The option Increment the build number after a successful build, on by default, bumps the version for you; building the same version again replaces that file, with a warning.

Versions on Disk lists every package of the module in Packages, with its build date, size, entry count and signature, and which one loads. It will not delete the package that is mounted.

Packing reads the files from disk, so save in the editor before you build. Source control and build folders (.git, bin, obj and the like), Logs, Profiles and scratch files such as .bak, .tmp and .psd are left out. A build fails if two paths differ only by case, or if a .gltf keeps its buffers in a separate .bin: export those as .glb.

The command line

For scripted or repeated builds there is a standalone tool, hydpack. It is not part of the game install: it lives in the engine source under Tools/hydpack/ and runs with dotnet run --project Tools/hydpack -- <command>.

hydpack keygen  <out.key>
hydpack pack    <moduleDir> <out.hyd> [signing.key]
hydpack inspect <package.hyd>              header, signature, boot order
hydpack verify  <package.hyd>              hash every entry
hydpack compare <package.hyd> <moduleDir>  byte-compare against a source tree
hydpack list    <package.hyd> [substring]  entries, largest first
hydpack probe   <modelDir|file>            can each model be imported from a package?

Exit codes are meant for scripting: 0 succeeded, 1 failed or bad package, 2 bad usage. verify fails if either the signature is invalid or any payload hash does not match — checking one without the other proves very little.

compare is the one to reach for when a packaged module misbehaves and a loose one does not. It tells you whether the package actually contains what you think it does.

Signing

Signing uses an ECDSA P‑256 key over the hash of the package index, which in turn holds a hash of every entry. Generate a key with hydpack keygen, which prints its fingerprint (XXXX-XXXX-XXXX-XXXX). Then, with the server stopped, set <package-signing-key> under <server> in config.xml to the key file's path, so the editor can sign what it builds. If that path names a file that is missing, the editor builds unsigned; the completion notice says (unsigned), so read it.

Be precise about what a signature proves. The public key travels inside the package, so a valid signature says the package is unaltered since it was signed by the holder of some key. Who that is, the fingerprint tells you, compared against one the author has published.

Guard the key. keygen deliberately refuses to overwrite an existing key file, because silently replacing one destroys an identity — every package signed afterwards carries a different fingerprint, and anyone who pinned the old one sees a mismatch with no explanation.

Signing is not copy protection, and it is worth being clear about that. Anything the browser renders, the browser can save; the engine has to be able to open packages, so it holds what it needs to. What signing genuinely provides is integrity and authenticity.

A package is read-only. Permanently.

This is the part to understand before you package anything, because it is absolute:

  • A .hyd is written once and never modified, appended to, patched or repaired in place.
  • There is no unseal and no overlay that shadows package content for authoring.
  • The editor is hard‑locked against packaged modules. Every save path refuses. The module list marks the row (packaged — read‑only) before you start editing, and a save attempt returns a specific refusal explaining why rather than a generic failure.

To change a packaged module you edit its source tree and build a new package. Which means the rule that matters is: keep the source tree. A package is an output, not a storage format, and it is not a backup.

The dev loop: loose beats packed

Packages live in /Packages and are discovered separately from /Modules. When both contain a module with the same id, the unpacked one in /Modules wins, unless you have pinned a package version (see below).

That is deliberate and it is your development loop: keep your source tree in /Modules while you work, and the package you built earlier is simply ignored. You do not have to move or delete anything to get back to editing.

To test the package itself, pick its version in the Version column of Hydra's modules list (below), or move the loose tree aside and restart.

Which version loads

When several packages in /Packages share a module id, the highest version wins. Versions compare numerically part by part, so 1.0.10 beats 1.0.9; a tie goes to the newest build. The filename is never read: the module.xml inside the package decides.

The Version column in Hydra's modules list overrides that. It offers the source tree and every packaged version on disk; choosing a package version pins it, and a pin outranks the source tree too, until you set it back. A pin to a version that is no longer there falls back to the highest. The pin is saved in config.xml and takes effect at the next module load.

Importing a module

Import Module, beside + Create Module in Hydra's modules list, takes a .hyd or a .zip of a module folder and puts it where it belongs: a package into Packages/<ID>-<version>.hyd, a zip into Modules/<ID>. Modules reload afterwards. It refuses, and says why, while a mission is playing; when a package fails verification; when Packages already holds that id and version; and, for a zip, when the id is already loaded or its folder already exists. A missing signature is a warning, not a refusal.

Steam Workshop

A packaged module can be published to the Steam Workshop and subscribed to like any other Workshop item. Both ends need the Steam client running and signed in on the host machine, the one running the server.

To publish:

  1. Load the module from its source tree, not from a package.
  2. Build the package, ideally signed, and test it.
  3. In Versions on Disk, press Publish on the version you want. This only works from a browser on the server's own machine. Add a change note in the Steam Workshop panel if you like.
  4. What goes up is the .hyd alone, with the module's name and description, and its Image, Banner or Icon (PNG, JPG or GIF under 1 MB) as the preview. If the module has a parent that is on the Workshop, it is added as a dependency.
  5. The first publish creates a private item and writes workshop-item="…" into your source module.xml. Keep that attribute: it is what makes the next publish an update rather than a new item. Make the item public from its Steam page when you are ready.

On a subscribing host, a Workshop module is mounted in place from Steam's folder, and only a .hyd at the top of the item counts. It appears in the modules list switched off, marked (workshop — read‑only): switch it on and apply. A local source tree or a package in /Packages with the same id outranks the Workshop copy, whatever the versions say. An item that arrives while the server is running needs a module reload or a restart. A host without Steam gets nothing from the Workshop; use Import Module there.

Scripts in a package

Mission scripts are held to the same compile‑time rules wherever a module comes from; see Mission Scripting. Two things are particular to packages:

  • Only inline scripts survive packaging. A script written inside the mission XML travels with it. A mission's separate .cs file and the module's Scripts/Shared folder are not loaded from a package.
  • Workshop scripts stay off until the signer is trusted. A Workshop module's missions load and run, but without their C# scripts, and the modules list marks it (scripts off). Scripts run only when the package is signed, the signature and every entry check out, and the signer's fingerprint is listed in the host's config.xml:
    <trusted-signers>
      <signer fingerprint="XXXX-XXXX-XXXX-XXXX" />
    </trusted-signers>
    There is no button for this: edit the file with the server stopped. Publish your fingerprint where your players can check it. Trust is not asked of loose modules, local packages or imported ones, and it covers scripts only: a module's web files reach every console regardless.

Runtime data does not live in the module

Because a package cannot be written to, anything the game generates had to move out of module trees: mission logs and player profiles are written to Logs and Profiles at the installation root, not inside the module they came from.

There is no package sidecar and no per‑package data directory. If your module expects to write something into its own folder at runtime, it will not work packaged.

What the container is

Enough detail to reason about it, for anyone who needs to:

[0]     Header, 64 bytes  — magic "HYD1", version, entry count, index offset and hash
[64]    Blob region       — payloads, 8-byte aligned, ordered by boot priority
[...]   Index             — path, offset, length, compression method, flags, hash
[EOF]   Trailer           — public key and signature (signed packages only)

Entries are plain (offset, length) spans in one file, read positionally. That is the property the format exists for: any number of readers in parallel, no locking, no per‑reader state — which is what a web server serving thousands of assets to a dozen consoles actually needs. It also keeps HTTP range requests working, so large video and music still seek.

Boot order puts module.xml first, then World/, then remaining XML, then HTML, then scripts and stylesheets, with bulk media last.

Only text is compressed — scripts, markup, XML, CSS, JSON, shaders. Images, audio, video and models are stored uncompressed because they already are compressed, and deflating them costs processing on every read for no gain.

The index and the payloads are also scrambled by default, with a key built into the engine. That raises the floor a little further; it is not secrecy, for the same reason signing is not copy protection.

A packaged module produces byte‑identical asset keys to a loose one, which is why everything else in the engine cannot tell the difference — including module layering. A package takes part in the same later‑wins precedence as any other module.

Before you ship

  1. Check the version in the manifest (or let the build increment it).
  2. Save everything, then build the package.
  3. hydpack verify — signature and every payload hash.
  4. hydpack compare against the source tree.
  5. Load the package rather than the loose tree (pin its version, or move the tree aside and restart), and play it. A package that boots is not the same claim as a package that plays.
  6. Put the loose tree somewhere safe. It is the only way to ever change this again.
Last updated 19 October 2026