Outward
Install with App

Details

Latest version
0.1.13
Last Updated
First Uploaded
Downloads
416
Likes
1
Size
359KB
Dependants

Categories

DangerousRoads — Aurai's wilds are not empty anymore

⚠️ Alpha, and single-player only for now. Multiplayer is not tested — see Known issues. Back up saves you care about.

Aurai's overworld is hand-placed and finite. Once you've cleared the road between Cierzo and the Vigil Pillar it stays cleared, and the walk back is a walk through an empty map.

DangerousRoads repopulates it. While you're travelling out in the wilds, hostile creatures can turn up anywhere — out of sight, off the road, behind you. Every stretch of country is country you have to watch again.

They're ordinary wild creatures native to the region you're in: Enmerkar sends beast golems and bandits, Abrassar sends bugs and bandits. They wander, they notice you, they fight, and they drop their normal loot.

What it won't do

  • Never in towns or villages. Settlements stay safe.
  • Never in dungeons, caves or interiors. Only out in the open.
  • Never touches your save. These creatures are temporary — they're gone when you leave the area, and nothing of them is written to your save file.
  • No bosses, no story characters, no custom content. Everything it puts in the world is vanilla.

Installing

Install from Thunderstore and the dependencies come with it. By hand: drop ForgeKit/, NetKit/, DonorKit/, CompanionKit/, SpawnKit/ and DangerousRoads/ side by side into BepInEx/plugins/. All five are hard dependencies — BepInEx refuses to load a plugin whose hard dependency is missing, so leaving one out means DangerousRoads silently never starts. (DangerousRoads names ForgeKit, SpawnKit and CompanionKit directly; NetKit and DonorKit come in underneath those two.)

Then just play. Nothing to turn on, no key to press — walk out of town and give it a minute.

Settings

BepInEx/config/cobalt.dangerousroads.cfg, created on first launch. The handful worth touching:

[General]
Enabled = true              ## false = mod off

[Schedule]
MinIntervalSeconds = 20     ## shortest gap between encounters
MaxIntervalSeconds = 60     ## longest gap — raise both for a quieter world

[Wave]
MinCount = 1                ## how many creatures show up at once
MaxCount = 3
SkipWhileInCombat = true    ## hold off while you're already fighting

[Placement]
MinDistanceMeters = 40      ## how close they can appear
MaxDistanceMeters = 200     ## how far away

[Notify]
ShowToast = true            ## the on-screen warning
[Compass]
ShowBlips = true            ## the compass markers

To turn it off: set Enabled = false and relaunch. To uninstall, delete the DangerousRoads/ folder from BepInEx/plugins/ — nothing of it lives in your save.

⚠ An existing cfg always wins over a new default. BepInEx writes the file once and never migrates a changed default into it, so a value you (or an older version) put there keeps applying silently. Each entry carries its shipped default in a # Default value: comment right above it — if behaviour doesn't match this README, compare those first. Delete the file to regenerate it from scratch (which also discards every other tweak).

Co-op: the room-wide warm predicate

SpeciesRoster used to ask Spawner.CanMintNow(key) — "is a body resident on this box". It now asks CanMintNow(key) && Spawner.CanMintRoomWide(key), because a species the host holds and a guest does not costs that guest a 3.6–14.7 s donor load mid-fight (the disconnect bug). CanMintRoomWide subsumes the local half, but both are named at the call site on purpose: this is the predicate the whole mod rests on and "local and room" is what it must be read as.

  • SpawnableNow() re-asks both halves every call (SpawnKit's cache is a 12-entry LRU, and the peer list moves too) and records LastShrink: species dropped purely because a peer is cold. A locally-cold species comes back ColdLocally and is never reported as a peer's fault.
  • RosterOrder.ForRoomWarm / ForPrewarm (pure, in DangerousRoads.Core) rank the region by how much spawnable roster the work buys: tier 0 = the party lacks it but the host holds it (one sk.want, free), tier 1 = nobody holds it (real local harvest), tier 2 = already room-wide. Ties keep source-trust order, so ForPrewarm is byte-identical to the pre-co-op order for any snapshot, not just an empty one.
  • AmbushDirector.ArmForRegion pushes Spawner.RequestRoomWarm(top N) after resetting the prewarmer, and the wave path re-pushes LastShrink when a wave is blocked. Neither is throttled here — SpawnKit's want book owns the repeat rule ([Coop] RoomWarmRequestTimeoutSeconds and the doubling backoff); a second DR timer would just be a second, differently-wrong opinion.
  • ReportShrink prints one [ROSTER] line per distinct (region, species+reason+actors) situation (RosterShrink.KeyFor canonicalises it) plus one degrade line per region when RoomWarmMode is RoomDegraded, and fires one Toasts.RosterShrunk per region arm. roadsroster gains the room glyph column (● / ◐ / ○).

No DR config. The mode knob is SpawnKit's [Coop] RoomWarmMode in BepInEx/config/cobalt.spawnkit.cfg:

[Coop]
RoomWarmMode = RoomDegraded

Built 2026-08-20, not live-verified — docs/guest-mirror-harvest-testplan.md GM10–GM19.

Event deck

Each time the timer comes due the director now draws a card instead of always running an ambush. Card #0 is the classic hostile wave; card #1 is the wandering merchant (its runner ships separately — until it is present, its weight is logged once, at the first draw, and ignored). Weights are relative shares of whatever is eligible; a card on cooldown leaves the pool entirely, so the ambush takes its draws and nothing falls through to "no event". With DeckEnabled = false, or with the ambush as the only enabled card, no roll is pulled and the mod behaves exactly as it did before. With every weight at 0 nothing fires at all (roadsstatus shows the soft block; roadsdeck shows drawable=0) — the deck never falls back to an ambush the operator weighted out. A card that throws mid-run is logged and treated as a soft outcome, so the director keeps ticking.

BepInEx/config/cobalt.dangerousroads.cfg:

[Events]
DeckEnabled = true              ## false = every event is an ambush (the pre-deck mod)
AmbushWeight = 1.0              ## relative share; 1.0 vs 0.35 below is ~74% ambush
MerchantWeight = 0.35           ## 0 = no merchants
MerchantCooldownSeconds = 600   ## stamped at DRAW, not on success

Pure draw logic: core/DangerousRoads.Core/EventDeck.cs (unit-tested, incl. one-card parity). Game side: Events/EventDirector.cs (cards from config, last draw, Register(IEventCard) seam), Events/AmbushCard.cs (wraps WaveRunner.Run unchanged). Built 2026-08-22, not live-verified.

Wandering merchant (card #1)

A Travelling Merchant (Merchants faction, Mefino's trade backpack, friendly to the player) appears out of sight on the same verified ground an ambush would, then walks the road to a zone exit and despawns there. Talk to him: greeting → Shop (the vanilla shop, stocked from this region's caravan trader table) or leave. He is a tank that barely hits back (Health 450, Protection 20, DamageMult 0.25, targets Bandits only) so that when bandits raid him the player has to step in; once he has been hit and the fight has ended, his greeting switches to the thanks-for-the-rescue line. Not lootable, never saved; one on the road at a time; a region change drops him.

The 2026-08-22 live pass (Cobalt, legion) found him fighting the player's pet instead of walking ("he kept running from me even when I tried talking"), naked, jogging, and alone. Built the same day:

  • Protected. AggroTools.Protect(merchant, "dangerousroads.caravan") at spawn, Unprotect at despawn: the player's weapons, lock-on, pet and its combat anchor, and every Player-faction ally's AI detection treat him as untargetable. The raid's bandits get an explicit re-allow pair (TargetableOverrides.Set(raider, merchant, true, owner)), so they can still hit him. [Merchant] Protected (default true; protected=false on the card for a session that needs to hurt him by hand).
  • Stops to greet. While any player is within GreetRadius (4 m) or he is in his dialogue (NpcRegistry.IsInDialogue), the carrot is parked at his own feet, the stall clock is held like combat, and he turns to face the nearest player (LateUpdate); he walks on GreetResumeSeconds (3 s) after the last player leaves. Core GreetPause; [MERCHANT] paused to greet / resumed the walk once per edge. WalkSpeed default dropped 1.1 → 0.5 — the value is AISWander.SpeedModif, a multiplier on the body's run speed (vanilla wanderers 0.3, SideLoader 1.1 = a jog).
  • The raid. Merchant/RaidRunner on the body: rolled once at spawn (RaidChance 0.75, RaidMin– RaidMax 2–3 raiders), springs when a player comes within RaidTriggerMeters (90 m) or after RaidDelaySeconds (120 s) so it is witnessed. Species from the region roster (warm, not blocked), preferring the Bandits faction, then any non-Merchants/non-Player faction, then unknown (Core RaidPlan.PickSpecies); placed on a 15–30 m ring (ClusterPlan.Offsets → NavProbe.SnapToWalkable); Spawner.Spawn with this mod's OwnerTag so MaxOwnActive, the far cache and roadssweep treat them as wave members — the count is clamped to the slots left under MaxOwnActive first (Core RaidPlan.CapToRoom; a full cap = no raid, logged); each one AggroTools.ForceTargeted onto him once its handle is alive. Toast "Bandits are closing on the merchant!" (its own throttle, so the merchant toast half a second earlier cannot swallow it). The rescue edge is unchanged (hit + combat exit). Despawn sweeps every override owned by dangerousroads.caravan (ClearOwnedBy), so a region change mid-raid — where neither Unprotect nor the runner's OnDestroy can see the body — leaks nothing. roadsmerchant raid springs it now; roadsmerchant status shows raid=armed/RAIDING/done.
  • Dressed. NpcSpec.RandomVisuals = true + OutfitPool from Merchant/MerchantOutfits (seven combos of Trader / Master Trader / Adventurer / Pilgrim / Padded / Worker / Scholar kit, by display name, checked against data/registries/items.json); outfit=<index> pins one.

The body is StoryKit's (NpcSpec → SideLoader template → Merchant graft → dialogue); DR decides where (AnchorRegistry.FindSpots), which exit (Core/RoadRoute.ChooseExit over the scene's InteractionSwitchArea census, preferring one ≥ MinRouteMeters away) and the route (NavMesh spawn → exit, threaded through the caravanner's roadside RandomPositionOnStart.SpawnPoints, each seed path-checked from the spawn and dropped when the navmesh cannot reach it). The walk is a carrot: an empty GO the SL wander state follows (AISWander.FollowTransform), advanced by Merchant/RoadWalker — combat states interrupt it and following resumes on their own. A stall toward an intermediate waypoint skips it (up to 3 per walk, Core/RouteSkip); a stall toward the exit gets one last straight try; only then does the walk end as stuck, and the warning carries the agent's own path state (pathStatus, hasPath, remainingDistance, isOnNavMesh, velocity, AI state, carrot position). The live stall of 2026-08-22 — "no progress for 90s toward waypoint 1/6 (16m away)", despawn at 134 s — is what this replaces.

Finding him. The toast names the bearing; the HUD compass also carries a green dot for him from the toast until he despawns, at any distance, drawn a little larger than the red hostile blips. That dot is this mod's. The other marker you may see in co-op is vanilla's own other-player compass element (OtherPlayersCompass), which points at your partner's character — not at the merchant, and not drawn by DangerousRoads.

BepInEx/config/cobalt.dangerousroads.cfg:

[Compass]
ShowMerchantBlip = true      ## live; the merchant's compass dot, independent of ShowBlips
MerchantBlipColor = #34C759  ## live; green. Unparseable = green, never red

BepInEx/config/cobalt.dangerousroads.cfg:

[Merchant]
Health = 450              ## next spawn; he must survive a raid until you arrive
Protection = 20           ## next spawn
DamageMult = 0.25         ## next spawn; if he clears a raid alone there is no rescue
WalkSpeed = 0.5           ## live; SpeedModif multiplier — 0.3 vanilla amble, 1.1 SideLoader jog
ExitRadius = 6            ## live; "arrived" = despawn. <= 0 never arrives
StuckSeconds = 90         ## live; no progress outside combat for this long = despawn. 0 = never
MinRouteMeters = 150      ## next spawn; prefer an exit at least this far from the spawn spot
DefendsHimself = true     ## next spawn; false = fully passive
GreetRadius = 4           ## live; stops + faces a player this close (0 = never; dialogue still holds him)
GreetResumeSeconds = 3    ## live; walks on this long after the last player leaves
Protected = true          ## next spawn; the player side cannot hurt/target him; bandits can
RaidChance = 0.75         ## next spawn; 0 = the lone walker, 1 = always raided
RaidMin = 2               ## next spawn
RaidMax = 3               ## next spawn; each raider counts against [Wave] MaxOwnActive
RaidTriggerMeters = 90    ## live; springs when a player is this close (0 = delay only)
RaidDelaySeconds = 120    ## live; springs this long after spawn regardless (0 = proximity only)

Files: Events/MerchantCard.cs (spec, spawn + roadscard overrides, route validation, rescue greeting swap, despawn, LivePosition for the dot; attaches the walker and the raid runner), Merchant/RoadWalker.cs (carrot, skip/last-try/stuck policy, stop-to-greet, diagnostics, rescue edge), Merchant/RaidRunner.cs (the raid), Merchant/MerchantOutfits.cs (the outfit pool), Merchant/RoadExits.cs (exit census + roadside seeds), Merchant/CaravanStock.cs (stock-table name + roadsstock census); pure Core/WalkProgress.cs (stuck tracker + rescue edge), Core/RouteSkip.cs (the skip policy), Core/GreetPause.cs, Core/RaidPlan.cs, Core/CardArgs.cs (the key=value parser) and Core/MerchantText.cs (the lines), all unit-tested. Log tag [MERCHANT]. Built 2026-08-22, not live-verified — docs/road-merchant-testplan.md.

Card #2: the town-guard patrol

Two or three town guards in the region's colours fighting four to six troglodytes on the road, with the guards in REAL danger without the player — that danger is the card, and it is a tuning number, printed on every [PATROL] RESOLVED line as guards N/M alive at X% health.

  • The guards are StoryKit human NPCs, not the game's town guards: those are static SNPC props with no Character and no AI (Monsoon's are the lone exception), so there is nothing to clone. Per-spawn specs dr_guard_<n>, Merchants faction, TargetableFactions = [Bandits, Mercs] (the two buckets the trogs live in), dressed from Patrol/GuardOutfits — a pool per Core/GuardLook.GuardTown, by display name, every one checked against data/registries/items.json and re-checked against ForgeKit's live item index once per session (roadspatrol looks). AggroTools.Protect(guard, "dangerousroads.patrol") shields them from the player side, exactly as the merchant is shielded and for the same live reason (a pet's anchor dragged him into combat).
  • The troglodytes are ordinary DR spawns: same OwnerTag, so [Wave] MaxOwnActive caps them (RaidPlan.CapToRoom), roadssweep clears them and the far cache parks them. Species come from Core/PatrolPlan.TrogSpecies — the seven spawnable keys, spelled exactly as SpeciesFactions.txt spells them (a unit test reads the embedded resource back) — intersected with the warm roster, with the full list as a logged fallback. The Queen is refused by GameObject name on the resolved handle (PatrolPlan.IsQueenBody): she has no species key of her own and shares Character.Name with the Mana Troglodyte, so nothing earlier in the pipeline can exclude her.
  • The wiring is pairwise, not by faction: TargetableOverrides.Set both ways per (trog, guard) plus AggroTools.ForceTarget in both directions with the RaidRunner 10 × 0.5 s retry, re-run every 5 s while Fighting for anything that lost its target to a death.
  • The phases are pure (Core/PatrolState): Spawning → Fighting → Resolved → Leaving → Gone, with a stalemate window and one hard rule — Resolved never re-enters Fighting, because the far cache can make a "dead" trog read alive again minutes later and the reward handshake must not re-run.
  • The reward is Core/PatrolLedger + PatrolPlan.Reward: uid-keyed spawned/dead/helped sets, a rate rolled once per encounter, and MarkPaid so a patrol pays at most once. "Helped" is Cobalt's rule — ANY player-side damage to ANY of the patrol's trogs — observed by Patrol/HelpWatch through three deliberately overlapping taps (Character.HasHit, Projectile.OnProjectileHit, and a per-trog OnHitPreResistance backup). Each tap is counted separately and the resolve summary prints the disagreement, which is the diagnostic if a real hit ever reads as no help. The row is a StoryKit Choice.Action on ONE surviving guard, computed at pay time, paid with Inventory.AddMoney (MP-safe: it routes an RPC for a guest).
  • Leaving and removal: after LingerSeconds (extended while anyone is in dialogue) the survivors walk to an exit chosen AWAY from the nearest player (dot-product over the census), on routes built by the shared Merchant/RoadRouteBuilder. Each guard is removed — Unprotect, pairwise overrides cleared, NpcRegistry.Unregister (which also drops its ChoiceActions) — the first tick it is farther than RemoveRadiusMeters from EVERY player (FarCache.FarFromAll, which returns false on an empty player list, so an unknown situation keeps the guard rather than blinking it out).

The runner (Patrol/GuardParty) lives on its own dr_patrol GameObject, NOT on a guard: unlike the merchant's RaidRunner, here the body it would ride is the one in danger, and a runner destroyed mid-fight would leave live trogs, live overrides and a registry entry with nobody to clean them.

BepInEx/config/cobalt.dangerousroads.cfg:

[Events]
PatrolWeight = 0.25       ## relative draw weight; 0 = never
PatrolCooldownSeconds = 900  ## stamped at DRAW, not on success

[Patrol]
GuardMin = 2              ## next spawn
GuardMax = 3              ## next spawn
TrogMin = 4               ## next spawn
TrogMax = 6               ## next spawn; counts against [Wave] MaxOwnActive
GuardHealth = 450         ## next spawn (template-baked)
GuardProtection = 12      ## next spawn
GuardDamageMult = 0.9     ## next spawn; under 1 so the party cannot wipe them before you can help
GuardChanceToAttack = 85  ## next spawn; percent
GuardProtected = true     ## at spawn; the player side cannot hurt/target them, the trogs can
RewardSilverMin = 5       ## live; per spawned-and-now-dead trog, IF you helped
RewardSilverMax = 6       ## live; rate rolled once per encounter
TrogRingMin = 12          ## live
TrogRingMax = 18          ## live
GuardRingMeters = 4       ## live
StalemateSeconds = 90     ## live; 0 = a fight must end by a wipe
LingerSeconds = 20        ## live; extended while a guard is in dialogue
LeaveSpeed = 1.4          ## live, read when the walk starts
LeaveTimeoutSeconds = 300 ## live; removal still waits for "far from every player"
RemoveRadiusMeters = 120  ## live; keep in step with [FarCache] DespawnRadius

Verbs: roadspatrol [status|spawn [key=value …]|resolve|help|leave|despawn|looks [town]], and roadscard patrol [key=value …].

Files: Events/PatrolCard.cs (town, placement, guard specs, exit choice), Patrol/GuardParty.cs (trogs, aggro, phases, reward, leaving, removal), Patrol/HelpWatch.cs (the three help taps), Patrol/GuardOutfits.cs (the per-town looks + the name check); pure Core/PatrolPlan.cs, Core/PatrolState.cs, Core/PatrolLedger.cs, Core/GuardLook.cs, Core/PatrolText.cs, all unit-tested. Log tag [PATROL]. Built 2026-08-26, not live-verified — docs/road-patrol-testplan.md.

Forcing a card with parameters: roadscard

roadscard <id> [key=value …] forces one card through the real Fire path with optional overrides. Every key is optional — an absent key is the card's normal random/config default, so roadscard merchant is exactly a deck draw of the merchant and roadscard alone is a real deck draw. roadsdeck <id> … and roadsmerchant spawn … accept the same tail. Unknown keys are warned once ([EVENT]/[MERCHANT] ignoring unknown key(s)) and the card still runs.

Card Key Meaning
merchant exit=<n> walk to exit n from roadsexits (out of range = refused, not defaulted)
dest=<text> walk to the first exit whose name contains text (no match = refused)
health= protection= damage=<mult> the body's numbers for this spawn (config defaults otherwise)
speed= walk speed for this merchant (overrides the live WalkSpeed for him only)
defends=true|false fights bandits / fully passive
at=here spawn 3 m in front of you instead of a hidden verified spot — "show me" mode
name=<text> display name
raid=0|1 forbid / force the bandit raid for this merchant (absent = roll RaidChance)
raiders=<n> raider count (absent = roll RaidMin–RaidMax)
raidspecies=<key> raider species (absent = roster pick, Bandits preferred)
outfit=<n> pin outfit n from the pool (0–6); out of range warns and stays random
protected=true|false override [Merchant] Protected for this spawn
patrol at=here spawn 8 m in front of you instead of a hidden verified spot
region=<town or region> dress the guards for that town (Berg, Abrassar, New Sirocco…)
guards=<n> trogs=<n> party sizes (absent = roll the config bands)
species=<key> pin the troglodyte species (absent = a warm one from the seven)
health= protection= damage=<mult> atk=<percent> the guards' numbers for this spawn
outfit=<n> pin outfit n from that town's pool; out of range warns and stays random
protected=true|false override [Patrol] GuardProtected for this spawn
exit=<n> the exit they leave by, from roadsexits (out of range = refused)
reward=<silver> pin the per-dead-trog rate instead of rolling RewardSilverMin–Max
ambush count= species= exactly roadsnow's forced count / species
roadscard merchant at=here health=50        # in front of you, fragile
roadscard merchant exit=0                   # walk to roadsexits #0
roadscard merchant dest=Cierzo defends=false
roadscard merchant at=here raid=1 raiders=2  # the raid, guaranteed, two bandits
roadscard ambush count=2 species=Hyena
roadscard patrol at=here trogs=4 guards=2     # a patrol in front of you
roadscard patrol species="Troglodyte Archmage" region=Berg

Dev verbs

Every verb below is read from BepInEx/config/DangerousRoads_cmd.txt — write the verb on a line of its own and it runs on the next poll; results go to BepInEx/LogOutput.log. Meant for debugging, not for play.

Verb What it does
roadsstatus director state: armed/disarmed, next-wave timer, last wave, last deck draw, what's blocking
roadsdeck the event deck: cards, live weights, cooldown remaining, last draw. roadsdeck draw forces a draw through the real path; roadsdeck <id> [key=value …] forces that card
roadscard [id] [key=value …] force one card with optional overrides (key table above); no id = a real deck draw
roadsmerchant [status|spawn [key=value …]|despawn|rescue|raid|goto <exit>] the wandering merchant: where he is (walk, greet pause, raid state) / put one on the road now (same keys as roadscard merchant) / remove him / fake the rescue edge (greeting swap) / spring the raid now / re-route to an exit index from roadsexits
roadsexits census of this scene's zone exits: index, destination, distance, bearing
roadsstock which live Merchant carries the caravan-trader stock table the road merchant copies
roadsroster this region's species roster, with anything BLOCKED marked
roadsfactions the faction each rostered species actually reports
roadsledger the anchor ledger — where waves have been placed and from what source
roadsanchors candidate anchors near you
roadssquadpoints vanilla AISquadSpawnPoints in range
roadsprobe / roadsmark probe a placement position / mark one for inspection
roadsui / roadsblips toast and compass-blip state (roadsblips counts hostile and merchant dots separately)
roadsnow force a wave immediately
roadsarm / roadsdisarm arm the director so the next wave is due now / stop the timer
roadswarm pre-warm species templates
roadssweep despawn everything this mod has out
selftest run the offline self-test ([SELFTEST] PASS/FAIL … DONE)

Known gap: roadsstatus throws a NullReferenceException if called on a fresh boot before the first wave has been computed. It self-heals once any wave fires.

⚠️ Known issues

  • A Wolfgang can turn up on the wrong side. The Wolfgang family's allegiance is driven by your plot and relationship progression in the base game, not by a fixed table value, so an ambush wave that draws one can spawn it in an allegiance the mod didn't intend — including friendly. The fix is to drop the whole family from the encounter roster (not to hard-code a faction for them, which would just be wrong at a different point in the story); that isn't in 0.1.0. If you hit it and want it gone now, add the names to [Species] ExtraBlocklist in the config and relaunch — Wolfgang Captain is already blocked by default, Wolfgang Veteran and Wolfgang Mercenary are not.
  • A few other species report an unstable faction. The Mantis family (Rock/Mana Mantis, Mantis Shrimp), Veaber and Caldera's Boreo have each been seen reporting different factions on different spawns. Under investigation; the practical symptom is the same as above — an encounter that doesn't fight you.
  • Multiplayer is untested. Only the host would ever run encounters, which is the right shape, but no real two-machine session has been played. Treat co-op as unsupported for now.
  • Encounter frequency varies by region. Some parts of the map give the mod more good places to put things than others, so the wilds are livelier in some regions than in others.
  • Three of the six regions have had a deep test pass; the rest have had less.

Compatibility

Outward must be on its Mono Steam beta branch, not the default IL2CPP build (Properties → Betas → select mono). If the game runs but no mods load and there's no crash log, this is almost always why.

Works alongside Beastwhispering and the rest of this mod family.

License

Apache License 2.0 — see LICENSE.

Thunderstore development is made possible with ads. Please consider making an exception to your adblock.