. Moons Code Setup

Updated a week ago

Code Setup

This page explains how to define a moon purely in code using DawnLib.DefineMoon(...) and the MoonInfoBuilder. It’s meant to show what a working moon needs, what DawnLib fills in for you automatically, and what optional extras you can add if you want more control.


What “defining a moon” means in DawnLib

When you call:

DawnLib.DefineMoon(moonKey, selectableLevel, builder => { ... });

you’re doing two things:

  1. Registering a NamespacedKey (UUID) for the moon (moonKey) and binding it to a SelectableLevel (selectableLevel).

  2. Using the builder to attach:

    • one or more playable scenes (required)
    • optional terminal / pricing logic
    • optional gameplay overrides (time, scrap, enemies)

The SelectableLevel is still the core vanilla container for the moon data. The builder layers DawnLib-specific behaviour on top (more scenes, predicates, etc) and allows you to override certain stuff incase supporting a config format to your mod.


What you must have for a working moon

1) A unique NamespacedKey<DawnMoonInfo>

This is the unique ID of your moon, and it must not collide with other content.

  • Namespace: your mod's name/ID (example: code_rebirth)
  • Key: your moon name (example: oxyde)

And you can create it using NamespacedKey<DawnMoonInfo>.From("code_rebirth", "oxyde");
This key is what other systems will use to reference your moon.


2) A valid SelectableLevel

You need a SelectableLevel ScriptableObject that’s already set up correctly in Unity (You can also create it in code and set it up there but it's a little big to reliably setup like that).

This is where your defaults come from (planet name, spawn curves, enemy power counts, scrap ranges, etc.). The builder can override some of these values if you want, but the asset itself still needs to be valid.


3) At least one unity scene registered via AddScene(...)

A moon with zero scenes is not playable. The builder will log an error if you forget.

A scene entry tells DawnLib:

  • which AssetBundle contains the scene (this means the scene HAS to be from a separate bundle to your normal bundles!)
  • The asset path to that scene in terms of folder structure (i.e. Path/To/Mod/Manager/plugins/MyMod/SceneBundle)
  • how the scene should be weighted if there are multiple scenes registered

The builder flow

A typical definition conceptually looks like:

  1. Add scenes (required)
  2. (Optional) Override terminal nodes
  3. (Optional) Create / Override terminal name keyword
  4. (Optional) Set a purchase predicate
  5. (Optional) Override cost
  6. (Optional) Override gameplay balancing values
  7. Done - DawnLib builds a DawnMoonInfo from everything you supplied

You don’t need to call Build() yourself - DawnLib does that internally.


Required builder calls

AddScene(...) (Required)

builder.AddScene(
    sceneKey,
    shipLandingOverrideAnimation,
    shipTakeoffOverrideAnimation,
    weight,
    assetBundlePath,
    scenePath
);

Adds a playable scene variant to the moon.

Parameters

  • sceneKey (NamespacedKey<IMoonSceneInfo>) Unique ID for this scene entry.
  • shipLandingOverrideAnimation (AnimationClip) Optional override used during landing. If you don’t use landing overrides, pass null.
  • shipTakeoffOverrideAnimation (AnimationClip) Optional override used during takeoff. If you don’t use takeoff overrides, pass null.
  • weight (ProviderTable<int?, DawnMoonInfo, SpawnWeightContext>) Controls how likely this scene is to be chosen relative to other scenes. This is a provider table, meaning it can be context-aware (weather, interiors, moon routing cost, etc.) if you want it to be.
  • assetBundlePath (string) Where the scene bundle is located (your mod-relative path).
  • scenePath (string) The Unity scene path inside the bundle (the .unity asset path).

Notes

  • You must add at least one scene.
  • If you add multiple scenes, DawnLib selects one using the provided weights.

Optional terminal setup

If you don’t touch any terminal methods, the builder will generate reasonable defaults.

OverrideRouteNode(TerminalNode node) (Optional)

Overrides the route confirmation node.

If you don’t set this, the builder creates a default route node with display text similar to:

  • “The cost to route to X is [totalCost]… Please CONFIRM or DENY.”

Use this if you want custom flavour text, formatting or behaviour.


CreateNameKeyword(string wordOverride) (Optional)

Controls the terminal keyword used to reference the moon.

If you don’t call this, the builder generates one automatically from SelectableLevel.PlanetName:

  • lowercased
  • spaces replaced / stripped to form a stable keyword
  • special characters removed

You would use this if:

  • your planet name contains characters that produce an awkward keyword
  • you want an alias keyword that differs from the planet name

Optional purchase gating (terminal logic)

SetPurchasePredicate(ITerminalPurchasePredicate predicate) (Optional)

Adds conditional logic controlling whether the moon can be purchased / routed to.

If you don’t provide a predicate, DawnLib defaults to:

ITerminalPurchasePredicate.AlwaysSuccess()

Use predicates when you want:

  • unlock requirements
  • conditional visibility/availability
  • routing restrictions based on progression or custom rules

Optional pricing

You can set cost in two ways: fixed or provider-based.

OverrideCost(int cost) (Optional)

Sets a static cost for the route.

This is just a convenience wrapper around the provider-based overload.


OverrideCost(IProvider<int> cost) (Optional)

Sets the moon’s cost using a provider.

If you do not override cost, the builder defaults to:

  • a provider wrapping _routeNode.itemCost

That means your route node’s itemCost becomes the default cost source if you don’t specify one.

Use a provider if you want:

  • context-aware pricing
  • dynamic pricing without replacing the entire terminal flow

Optional gameplay overrides

These modify the underlying SelectableLevel values directly.

You do not need any of them for a moon to function, but they’re the main “balance knobs” that mods usually expose.

OverrideTimeMultiplier(float multiplier)

Directly sets:

  • SelectableLevel.DaySpeedMultiplier

Use this to make the day pass faster/slower on your moon.


OverrideMinMaxScrap(BoundedRange range)

Directly sets:

  • SelectableLevel.minScrap
  • SelectableLevel.maxScrap

Use this to rebalance loot density.


OverrideEnemyPowerCount(int inside, int outside, int daytime)

Directly sets:

  • SelectableLevel.maxEnemyPowerCount
  • SelectableLevel.maxOutsideEnemyPowerCount
  • SelectableLevel.maxDaytimeEnemyPowerCount

Use this to control overall enemy intensity per category.


OverrideEnemySpawnCurves(AnimationCurve inside, AnimationCurve outside, AnimationCurve daytime)

Directly sets:

  • SelectableLevel.enemySpawnChanceThroughoutDay
  • SelectableLevel.outsideEnemySpawnChanceThroughDay
  • SelectableLevel.daytimeEnemySpawnChanceThroughDay

Use this when you want different pacing throughout the day (early pressure vs late pressure, etc.).


OverrideEnemySpawnRanges(float inside, float outside, float daytime)

Sets:

  • SelectableLevel.spawnProbabilityRange (inside)
  • _outsideEnemiesProbabilityRange (outside)
  • SelectableLevel.daytimeEnemiesProbabilityRange (daytime)

This is important because outside spawn range is tracked separately in the base game (_outsideEnemiesProbabilityRange, default 3f because zeekerss has it hardcoded to 3f by default) and then stored into DawnMoonInfo.

Use this to control how many enemies can be selected/spawned per cycle, especially outside.


What DawnLib auto-fills if you don’t set it

Inside Build(), the builder ensures these always exist:

  • Route node: auto-created if _routeNode == null
  • Receipt node: auto-created if _receiptNode == null
  • Name keyword: auto-created if _nameKeyword == null
  • Purchase predicate: defaults to AlwaysSuccess() if unset
  • Cost provider: defaults to _routeNode.itemCost if unset

It will also log an error if you defined a moon with 0 scenes.


Minimal vs “fully featured”

Minimal (playable)

A moon only truly needs:

  • a NamespacedKey<DawnMoonInfo>
  • a valid SelectableLevel
  • one AddScene(...)

Everything else is optional, because DawnLib can generate terminal nodes/keyword and defaults predicate/cost.

Fully featured (common additions)

DawnLib moon mods are able to have combinations of:

  • multiple scenes + weights (variants)
  • custom route/receipt terminal nodes (flavour/UI polish)
  • a purchase predicate (unlock rules)
  • explicit cost override
  • balance overrides (time, scrap, enemy counts, curves, ranges)

Common mistakes

  • Forgetting to add a scene Moon registers, but isn’t playable. Builder logs: “has 0 scenes.”
  • Wrong bundle path / wrong scene path Moon appears but fails at load time.
  • Not using a unique key Collisions can cause unexpected overrides or routing conflicts.
  • SelectableLevel missing required vanilla references This breaks like a normal custom moon would, even without DawnLib.