How the game decides what kind of room a given enclosed area is. Audience: modders editing
data/rooms/rooms.json or the trigger definitions in data/condtrigs/condtrigs.json.
How it works
IsPortal) are the boundary markers. Result: a Room with a tile list and a
synthetic Compartment CondOwner.
rooms.json is sorted by nPriority descending,
so high-priority specs are tested first.
Matches() returns true wins. Checks: pressurization gate,
tile-count bounds, all aReqs satisfied, no aForbids hit.
What "Value ×" actually does
Sale-price multiplierThe room's fValueModifier multiplies the base price of every installed CondOwner inside it when the ship's sale value is computed. It is not a stat boost, an atmosphere buff, or a passive resource — it's strictly a price multiplier on the contents.
- Blank ×1.0 — contents sell for their base price. No bonus, no penalty.
- LuxuryQuarters ×2.0 — every installed bed, bin, chair, and light in that room sells for twice its base price.
- Loose / uninstalled items are skipped: they're never added to the room's content list (
AddToRoomrequiresIsInstalled), so they don't pick up the multiplier. - Walls, floors, and other "props" CondOwners that satisfy the room's flood-fill but aren't installed equipment are also excluded for the same reason.
- The multiplier is the only thing the spec contributes to ship value — there's no flat bonus per room, just the per-item scaling.
So when you mod a new room with fValueModifier: 1.5, you're saying "anything installed in this layout is worth 50% more when the ship sells." Source: Room.cs:206 in the decompiled assembly.
The full ship-value formula
That's the entire calculation (Ship.cs:8632). Two contributors: per-room contents multipliers (already inside RoomValue) and a flat ×3 bump for having any O2 pump. No variety bonus — the game does not count distinct room types or reward diversity directly. The "variety effect" is implicit: spreading installed items across specialized rooms means each item picks up a higher multiplier than it would in a single Blank box.
GetPartsValue() is a separate "strip for parts" number — Σ StatBasePrice × damage_state across all installed COs, no room multiplier. Used for piecemeal sale, not whole-ship sale. Derelicts use GetShipValue() wrapped by a break-in factor and a deterministic-random per-ship modifier.
Walls and wall-mounted equipment
Asymmetric handlingWalls and wall-mounted equipment are routed to rooms differently — and a wall-mounted item's use point, not its actual position, decides which room credits its value.
Tile.AddToRoom(tile, co, addEffects): if (tile.room == null) // wall tile, no flood-fill room pos = co.GetPos("use", false) // ← fall back to use-point tile = ship.GetTileAtWorldCoords1(pos) if (tile == null || tile.room == null) return // still nothing → CO gets $0 tile.room.AddToRoom(co, addEffects)- Walls themselves contribute $0 to ship sale value.
ItmWall1x1isIsInstalledwithStatBasePrice: 21, but itsmapPointsarray is empty. With no "use" point, both branches above fall through and the wall is never added to any room'saCos. Walls do show up inGetPartsValue()(which walks installed COs directly), but not inGetShipValue(). - Wall-mounted equipment is credited to the room its
usepoint lands in. A heavy lift rotor (StatBasePrice: 56,774) sits on a wall tile, but its"use,0,-42"point lands on the interior floor tile a crew member would stand on. Whichever room contains that tile is the room that multiplies the rotor. - This means use-point routing is a value lever. Same heavy lift rotor whose use-point lands in a Blank room:
56,774 × 1.0. Same rotor whose use-point lands inside a LuxuryQuarters' floor area:56,774 × 2.0=113,548. The rotor doesn't move; only the interior layout around its interaction tile changes. - Equipment with a use-point that lands on another wall tile is worth $0 in ship sale (the second
returnfires). External-only equipment with no path to an interior tile contributes only viaGetPartsValue().
So the real maximization play isn't just "stuff a valuable item into LuxuryQuarters" — it's: lay the high-multiplier room out so its enclosed area swallows the interior interaction tiles of the highest-base-price wall-mounted equipment (engines, rotors, weapons, RCS thrusters), while still satisfying that room's reqs and forbids and not tripping a higher-priority spec.
Decision order
First match wins. Click a row to jump to its detail card; Esc to clear focus.
| Pri | Spec | Friendly | Min tiles | Value × | Pressurized? |
|---|
Per-room reference
Trigger glossary
The aReqs / aForbids strings are CondTrigger references with
=chance×count syntax, e.g. TIsChairInstalled=1.0x4 = "≥4 installed chairs".
A few are meta-triggers defined in condtrigs.json with bAND: false
that OR over a sub-list:
| Meta-trigger | Satisfied by any of |
|---|---|
| TIsRoomEngineering |
TIsCanister·
TIsChargerBattery04Installed·
TIsShipBatteryInstalled·
TIsRCSDistroInstalled
|
| TIsRoomWellnessOptionals01 |
TIsFridge01Installed·
TIsSinkInstalled·
TIsTreadmillInstalled·
TIsStrengthTrainerInstalled
|
| TIsRoomRecreationOptionals |
TIsTerminalInstalled·
TIsTVInstalled·
TIsBartopInstalled
|
| TIsRoomCargo |
TIsStorageBinInstalled·
TIsRackInstalled
|
| TIsRoomCargoExterior |
TIsCargoWeb01Installed·
TIsStorageBinInstalled
|
| TIsCanister |
TIsRTAInstalled·
TIsCanister01Installed·
TIsCanisterLH02Installed·
TIsCanisterLHe02Installed
|
| TIsShipBatteryInstalled |
TIsBattery02Installed·
TIsBattery02bInstalled·
TIsBattery02cInstalled
|
Plain (non-meta) triggers used in rooms.json bind directly to a CondOwner test:
| Trigger | Requires (aReqs) | Forbids |
|---|---|---|
| TIsReactorIC | IsReactorIC | — |
| TIsNavStationInstalled | IsNavStation, IsInstalled | IsDamaged |
| TIsTowingBraceInstalled | IsEquipmentTowing, IsInstalled | IsDamaged |
| TIsDockSysInstalled | IsDockSys, IsInstalled | — |
| TIsBedInstalled | IsCushion, IsSheet, IsInstalled | IsDamaged, IsSlotted, IsInContainer, IsCarried |
| TIsToilet | IsContainer, IsOpening10cmUp, IsWaterproof, IsInstalled | — |
| TIsSinkInstalled | IsSink, IsInstalled | IsDamaged |
| TIsTableInstalled | IsTable, IsInstalled | — |
| TIsChairInstalled | IsChair, IsInstalled | IsDamaged |
| TIsFridge01Installed | IsFridge01, IsInstalled | — |
| TIsStorageBinInstalled | IsStorageBin, IsInstalled | IsDamaged |
| TIsLightSourceInstalled | IsLightSource, IsInstalled | IsDamaged |
| TIsHatchInstalled | IsHatch, IsInstalled | IsDamaged |
| TIsRCSDistroInstalled | IsRCSReg, IsInstalled | — |
| TIsRTAInstalled | IsRTA, IsInstalled | IsDamaged |
Pairs and oddballs
Most rooms are exactly what the JSON says. Five cases need extra context that a naive read of the JSON won't surface:
PairBridge (Closed) vs Bridge (Open)
Both require only TIsNavStationInstalled. The classifier between them is the
forbid list: BridgeRoom (90, ×1.5) forbids RCS distro, recreation, wellness, toilet,
reactor, and bed; BridgeArea (80, ×1.3) forbids nothing.
A "clean" bridge gets the closed bonus; a Nav Station co-located with anything else falls through to
Open. This forbids-as-purity-tier pattern is unique to the Bridge pair — no other room
types share aReqs and split on aForbids. Copy this pattern when modding a new
specialized room with a graceful fallback.
OddballCargoRoomExterior — only bAllowVoid: true
The pressurization gate is binary: a spec wants either a void or a pressurized room, and an
unpressurized enclosed area can only be CargoRoomExterior or Blank. If you mod a new
void-permitting room, you must explicitly set bAllowVoid: true or the spec will never match.
GotchaReactor priority 100, no forbids
First-hit-wins plus no forbid list means an IC reactor turns any 4+-tile pressurized room into a Reactor regardless of what else is installed. Bed + reactor → Reactor (not Quarters). Nav Station + reactor → Reactor (not Bridge). For a coexisting "engineering bay with a reactor" classification you need a higher-priority spec that requires both the reactor and the engineering items.
HiddenMeta-trigger expansion — five rooms hide their real requirements
Reading the JSON literally, Engineering requires TIsRoomEngineering. That's
not enough for a modder — the actual requirement is any one of four sub-triggers. Same for
WellnessRoom, Recreation, CargoRoom, CargoRoomExterior. A site that auto-renders rooms.json
without resolving meta-triggers will look correct but be useless to a modder asking "what do I install
to get a Wellness Room?" The cross-reference resolver should follow aTriggers on
bAND: false triggers and render the OR-set.
Edge caseBlank — fallback, not a match
IsBlank is short-circuited inside Matches(), so Blank's aReqs,
aForbids, and nMinTileSize are dead fields. The row is just a holder for
strNameFriendly / strIconName / fValueModifier. Blank is assigned
via the fallback _roomSpec ??= dictRoomSpec["Blank"] at the end of
CreateRoomSpecs(), never picked by the priority loop.
Modding notes
- New room specs go into
comment_mod/data/rooms/(or a v2 mod overlay) on top ofdata/rooms/rooms.json. Specs are loaded intoDataHandler.dictRoomSpeckeyed bystrName; collisions last-wins. - New
aReqs/aForbidstriggers must already exist indata/condtrigs/condtrigs.json. You can't inline a CondTrigger inside a RoomSpec. nPriorityties resolve byOrderByDescendingalone, which is unstable. Give every new spec a unique priority to avoid load-order surprises.- The
aReqs/aForbidsstrings are parsed through the sameLootmachinery used elsewhere — a syntheticLootwithstrType="trigger". Same=chance×countsyntax as loot-table entries.
Generated from data/rooms/rooms.json, data/condtrigs/condtrigs.json, and the
decompiled classifier in decomp/Assembly-CSharp/Room.cs /
Ostranauts/Ships/Rooms/RoomSpec.cs.