SUNRISE STUDIO

How a build works

A build reads the project overlay, the same view the editor shows, and writes package files. It never writes into the installation. An install copies the build into a Sunrise mod folder. Packages describes the package format itself.

Build packages writes into a new folder build/<UTC time>-<random id> inside the project. An output folder must be empty and outside the installation. Before every build, Studio rebuilds three investment tables from the installed bytes; Investment editing internals says which. Object edits are compiled again from the installed originals, never read from an older build.

What the writer produces

The writer builds version-38 packages, the pre-Beyond Light layout this build uses.

changeoutput
new entries in a new package IDa new package, w64_sunrise_<id>_0.pkg
entries that object edits generatea new package of their own, named the same way
changed entries of an installed packagea patch: the next generation of that package
changed entries of a startup _unp1_ packagea full replacement with the installed file name and patch number

Package IDs gives the range a new package can use. An entry whose bytes equal its installed original is left out. A build where every entry is left out and nothing else changed stops with “nothing was built”.

New blocks are written uncompressed. Existing block records and required patch generations are kept. Every current package family in a checked installation uses the supported variant-1 metadata. Older variant-0 blocks may be inherited, but patching or replacing a variant-0 package is refused.

A startup replacement’s receipt lists the files it replaces.

The output needs Sunrise’s custom package trust support, because stock signatures cannot be made.

Dependency records

The game loads only entries that a dependency record names. A build keeps that true for the project’s changes.

  1. Each generated entry is added to every installed record that names its source.
  2. Each added entry is added to every record that names an entry pointing at it, by tag or by wide hash. This follows chains of added entries too.
  3. Each changed package’s records are encoded once.
  4. A package whose only change is its records gets a patch of its own.

A generated entry whose source no record names stops the build. An added entry that no record reaches is listed in the receipt under addedAssetsInNoRecord. New item and vendor entries are listed there on purpose: the client requests them by tag handle.

Records only gain entries, so two mods’ record edits to one package merge at startup. See Two mods patching one package.

Manifests

A root tag also loads the entries its manifest lists. An added entry joins each manifest that lists an entry pointing at it, when that manifest already lists entries of its class. The receipt lists these under manifestEdits, each with its reason. A new string bank joins the investment_globals manifest this way.

reasonan entry joins when
referencea listed entry, or an entry that already joined, points at it by tag or wide hash
native dependencya listed entry’s decoded fields name it
socket dependencyit is a new socket plug, or its display, of an item with bucket byte 2 or less

Native dependencies skip the class rule. An edited installed entry adds only the dependencies its installed original lacks, because its manifests already load the rest. A newly referenced installed entry brings its own dependencies. Socket dependencies always go to investment_globals, 0x81A2926F. A manifest that the project edits itself and that also needs new entries stops the build.

A new root entry gets a manifest of its own. The build copies the manifest of the installed root it was cloned from, adds the new root’s resident closure, and writes it into the same new package. The receipt lists it under rootManifests.

Wide-hash aliases

Only new packages carry aliases. A build writes each alias as a {uint64 key, uint32 tag, uint32 class} row in the alias table of the package’s misc region, class 0x80809D02. Rows are sorted by key, and a repeated key is refused. Named tags keep their rows and string offsets. A patch keeps the source misc region, apart from dependency record edits.

Merge data

Each built package gets a <stem>.merge file beside it. Startup _unp1_ replacements get none. Sunrise uses it to merge the package with other mods’ patches of the same package. Two mods patching one package says how. The binary layout is version 2, defined in Sunrise’s table_merge/sidecar.h.

The file gives each edited entry one kind:

kindgiven to
tablean entry of a table listed below, or any other installed entry edited at the same size
manifesta manifest that only manifest planning changed
addedan entry of a new package that names rows this build added
opaqueevery other installed edit, which changes the entry’s size

A table entry lists the installed bytes it changed and the SHA-256 of the installed entry. An entry of a listed table also lists each array Sunrise rebuilds, grown or not. A change outside those arrays, or a removed row, makes it opaque. A same-size entry outside the list has no arrays, so two mods’ edits to different 8-byte words of it both apply.

Table, opaque and added entries also list each field that names a row this build added. A field that names an installed row, or holds 0xFFFF for none, is not listed.

These installed tables merge by appended rows:

tabletaglimitpair group
item index0x81327CCB32,768item
item display index0x81613CF132,768item
icon index0x81A291C232,768
item art0x81A28B2332,768 itemsitem
collectibles0x81327CDE32,768collectible
collectible displays0x81327D6732,768collectible
presentation nodes0x8131933F32,768
sandbox perk definitions0x8131932732,768perk
sandbox perk displays0x81613D2832,768perk
socket types0x8131932F32,767socket_type
socket type displays0x81613D0632,767socket_type
lore text0x81613CF232,767lore
lore rows0x8131934832,767lore
stat groups0x81613D08128
gear0x81613D2732,768gear
gear definitions0x8131932632,768gear
arrangements0x81613D2332,768arrangement
arrangement definitions0x8131932932,768arrangement
vendor index0x8131931D32,767vendor
vendor display index0x81A2926D32,767vendor
string bank index0x81A2721132,768

Tables in one pair group must grow by the same count. A build that breaks this stops. Item hashes, collectible hashes and bank name hashes are marked unique.

These installed tables merge as sorted maps, keyed by the u32 at row +0: the runtime name map 0x80EC3F60, the art name map 0x80EC3F61 and the HUD icon library 0x80EFC07B.

SunriseStudio.Projects/TableMergeRecipes.cs holds both lists and the rules that find row references. Each build also writes merge-sidecars.json, a readable copy of every .merge file with each entry’s kind and the reason an entry is opaque.

The build receipt

Each build writes build-receipt.json into its output folder.

fieldholds
packagesthe output files
addedAssets, generatedAssetsthe new tag handles and their package files
configBindingsthe config binding sites
objectEditseach object edit, its change and the tags it wrote
objectEditNotesnotes from compiling the object edits
sourceAssetsNotBuiltadded assets with no native layout
dependencyEdits, dependencyTablesSkippedeach record edit, and the tables that could not be read
addedAssetsInNoRecordadded entries that no record reaches
manifestEditseach manifest an added entry joined, and the reason
rootManifestseach new root’s manifest, the installed manifest it copies, and the entries it adds and keeps
requiredPredecessorsthe installed packages the patches need
replacesInstalledFilesthe installed startup files that full replacements stand in for
mergeSidecarseach .merge file, its package and its SHA-256

Installing

Build and install checks the mod details, builds, stages the result in a folder whose name starts with ., then replaces bin/x64/Sunrise/mods/<mod id> with it. Sunrise ignores folders that start with ., so the staged copy stays inert until the swap. The old install is renamed aside and deleted only after the swap. If the game holds the folder open, the install stops and asks to close the game. A project with only scripts or SDK modules installs without building packages.

sourcegoes to
new packagespackages
patches of installed packagespatches
.merge filesbeside their package
Lua controllers and helpersscripts
own SDK modulessdk
mod detailsmod.json

The install writes only under bin/x64/Sunrise/mods, never into packages. It keeps an ownership receipt of the script files it installed, so Update running game can replace and remove its own files and leave files added by hand.

Updating a running game

Update running game builds the same way, replaces only the changed script files, then sends one reload_mod command over the command link. The live link describes the command.

Only scripts reload. The game reads packages, patches, SDK modules and mod.json once at boot. If any of them changed, the update stops and asks for Build and install with the game closed. The mod must be installed once before it can be updated. Exactly one game must run from this installation.

Limits

  • A startup replacement cannot install as a mod.
  • A record change that is not an addition cannot merge with another mod’s patch of that package.
  • A patch number stays below 255.
  • A package holds at most 8192 entries and 16384 blocks.
  • A package holds at most 1,000,000 alias rows.