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:
- Load the module from its source tree, not from a package.
- Build the package, ideally signed, and test it.
- 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.
- 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.
- 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:
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
- Check the version in the manifest (or let the build increment it).
- Save everything, then build the package.
hydpack verify — signature and every payload hash.
hydpack compare against the source tree.
- 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.
- Put the loose tree somewhere safe. It is the only way to ever change this again.