Skip to content

feat(Building): interior systems, NPC navigation, terrain prep, and post-build customization - #6

Merged
ifBars merged 65 commits into
ifBars:stablefrom
hdlmrell:stable
Mar 30, 2026
Merged

ifBars merged 65 commits into
ifBars:stablefrom
hdlmrell:stable

Conversation

@hdlmrell

@hdlmrell hdlmrell commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Disclaimer: The summary of this PR was written by Claude and audited/edited by me.

This PR adds the building systems needed to make S1MAPI buildings feel like real game locations: interior walls, roofs, terrain preparation, NPC navigation, and post-build customization. It also hardens existing wall/decor geometry and fixes several IL2CPP compatibility issues.

34 files changed — 14 new files, 20 modified. ~8,200 lines added across 45 commits.

What's new

  • Interior walls with door openings and automatic doorway tracking (InteriorWallBuilder)
  • Roofs — parapet (commercial flat-top) and hip (residential slope) styles (RoofBuilder)
  • Terrain preparation — height flattening under footprint (TerrainFlattener) and tree/vegetation/object clearing (TerrainClearer), both with IL2CPP ICall support and auto-retry for late-loaded terrain
  • NPC interior navigation — custom A* pathfinding grid, Harmony-patched SetDestination interception, multi-phase doorway entry/exit with stair lerping, combat-gated chase detection, directed NPC routing API (NavigationBuilder, InteriorNavigatorCore, InteriorPathGrid)
  • Part registry — semantic query and bulk material swap by building part category or wall direction (BuildingPartRegistry)
  • Dual-material walls — exterior walls with separate interior/exterior face materials via two-submesh mesh (WallAppearance, DualMaterialBoxGenerator)
  • Networked prefab linking — client-side FishNet prefab re-parenting for multiplayer door sync (NetworkedPrefabLinker)
  • Stair styles — Solid, ClosedRiser, and OpenStringer variations (StairStyle enum)
  • Constants system — centralized tuning values in nested static classes (Constants.cs)

What changed in existing code

  • BuildingBuilder — new fluent methods: AddInteriorWall, AddParapetRoof, AddHipRoof, AddCornerTrim, AddStairs, AddDoorFrames, AddInteriorDoorFrames, FlattenTerrain, CreateNavigationBuilder, WithInteriorWallLayer. Exposes Registry and GridCellSize properties.
  • WallBuilder — per-wall WallAppearance overrides, multiple windows per wall, WallOpening.Offset for off-center placement, DoorWithWindows factory, corner gap fix (N/S walls extend to cover E/W corners)
  • DecorBuilder — stairs, corner trim, door frames (exterior + interior), base molding gaps around doors, molding protrusion fix
  • PrefabPlaceronCreated callback, networked prefab queue integration
  • PrefabRefInstantiateNetworked() for FishNet-spawned prefabs
  • LightingBuilder — URP-correct light defaults for Schedule I's pipeline
  • ProceduralMeshBuilder — all rendering layers enabled for decal projection support
  • IL2CPP fix — replaced foreach (Transform t in ...) with index-based GetChild() loops across extensions

Breaking changes (binary)

Adding default parameters to existing public methods is a binary breaking change — consumer mods compiled against the old signature must recompile. Source compatibility is preserved (no code changes needed, just rebuild).

Affected methods:

Method Change
BuildingBuilder.AddFoundation Added Color? color, Material? material
BuildingBuilder.AddBaseMolding Added bool skipWalls
BuildingBuilder.AddSlidingDoors Added Action<GameObject>? onCreated
BuildingBuilder.AddPrefab Added Action<GameObject>? onCreated
DecorBuilder.AddBaseMolding Signature expanded with skipWalls + door gap logic
WallOpening.Door Added float offset
WallOpening.Window Added Count, DividerWidth, GlassMaterial, FrameColor, FrameMaterial

Areas likely to be scrutinized

Harmony patch on NPCMovement.SetDestination

The interior navigator installs a prefix patch that intercepts every NPC SetDestination call game-wide. If the destination resolves inside a managed building, the original call is blocked and custom A* routing takes over. This is the most invasive change in the PR.

  • Why it's justified: There's no other way to route NPCs through interiors. The game's NavMesh has no data inside buildings (it's carved out). Without interception, NPCs would path along the carving boundary and get stuck at corners.
  • Safeguards: The patch only activates when a NavigationBuilder exists. Non-building destinations pass through unmodified. Combat detection uses reflection (no compile-time ScheduleOne dependency) with graceful fallback. The patch is removable via NavigationBuilder.Remove().
  • Risk: If the game renames NPCMovement or SetDestination, the patch silently fails (no crash, NPCs just won't enter buildings). The reflection chain for combat detection (NPCMovement.npc → NPC.Behaviour → activeBehaviour) could break on game updates.

Reflection-heavy NPC code

InteriorNavigatorCore uses reflection for: NPC speed, NavMeshAgent access, destination clearing, combat behavior detection. All accessed via MemberAccessor with IsValid checks and try/catch fallbacks. This is by design per coding standards (no ScheduleOne compile-time deps), but it's a lot of reflection surface area that could break on game updates. It might be worth having a conversation about.

TerrainFlattener IL2CPP ICalls

TerrainFlattener uses raw Unity ICalls (TerrainData::GetHeights_Internal, SetHeightsDelayLOD_Injected) on IL2CPP because the managed TerrainData wrappers crash. This is fragile — Unity version changes could alter ICall signatures. The Mono path uses normal managed APIs. To my knowledge, this is the only way.

Terrain clearing heuristics

TerrainClearer identifies vegetation by name keywords (bush, tree, grass, etc.) and protects roads/fences by keyword. Misclassification is possible if a mod adds objects with unexpected names. The Filter callback is the escape hatch.

Magic number density

Navigation thresholds, phase transition distances, and stuck detection timers are now in Constants.InteriorNav, but the overall system has many tuning values that were calibrated for specific building sizes. Edge cases with very large or very small buildings may need threshold adjustments.

AddWalls signature expansion

The AddWalls method and WallOpening factories gained many optional parameters. While source-compatible, the parameter count is getting high. Future work might benefit from an options object pattern.

Release Notes

  • Interior systems: InteriorWallBuilder (interior walls & doorways), InteriorPathGrid (A* grid), InteriorNavigatorCore (multi-phase NPC traversal) and NavigationBuilder orchestration (stair handling, doorway thresholds).
  • Roofs: Parapet (Deep/Shallow presets) and Hip roofs with custom mesh builders and material fallbacks.
  • Terrain: TerrainFlattener (height blending, detail clearing, IL2CPP ICall support, auto-retry) and TerrainClearer (vegetation/scene-object clearing with preserved-filter support and retry queue).
  • NPC navigation: Harmony prefix on NPCMovement.SetDestination to route managed NPCs through doorways; NavigationBuilder installs ramp/carving obstacles and an InteriorNavigator shell.
  • Post-build customization: BuildingPartRegistry for semantic querying and bulk material/submesh swaps; Registry exposed on BuildingBuilder.
  • Prefabs & networking: PrefabPlacer networked placement refinements, InstantiateNetworked overloads (including inactive spawn), and NetworkedPrefabLinker (deferred client-side reparenting for FishNet).
  • Structural & decor: stairs (Solid, ClosedRiser, OpenStringer), corner trim, door frames, segment-aware base molding, foundation height tracking, and stair variants.
  • Walls & windows: dual-material two-submesh walls, multi-pane windows with dividers, DoorWithWindows factory, per-wall WallAppearance overrides, expanded WallOpening options (offsets, glass/frame materials).
  • Builder & API expansions: many fluent/applied methods added or extended (e.g., AddInteriorWall, AddParapetRoof, AddHipRoof, FlattenTerrain, CreateNavigationBuilder, AddStairs, AddInteriorDoorFrames), plus BuildingPalette interior-wall fields and BuildingBuilder.Registry/GridCellSize.
  • Constants: centralized tuning constants for Window, Roof, NavMesh, InteriorNav, InteriorWall, Terrain, Materials, Spatial, Layers/Tags.
  • IL2CPP hardening: replaced foreach-on-Transform with index/GetChild loops; TerrainFlattener uses native ICalls on IL2CPP with safe fallbacks and TerrainRetryQueue; project file adds Mono/Il2cpp assembly names and explicit Harmony references.
  • Breaking / noteworthy API changes: several expanded method signatures with added default parameters requiring consumer recompilation (notable: BuildingBuilder.AddWalls overload/return change, WallOpening.Door/Window signatures, AddBaseMolding expansion, PrefabPlacer.Place onReady callback, PrefabRef.InstantiateNetworked overloads). New NavigationBuilder creation (BuildingBuilder.CreateNavigationBuilder()) and properties (InteriorDoorways, Registry). Areas needing focused review: Harmony NPCMovement patch, reflection-heavy NPC access, IL2CPP ICall usage, terrain-clearing heuristics, and navigation tuning constants.
  • Documentation: API overview, building guide, and examples updated; new StorefrontSpawner example demonstrating terrain clearing, flattening, nav builder, and networked doors. Coding standards updated to forbid compile-time ScheduleOne dependencies.

Summary: 34 files changed, ~8,235 lines added and ~304 lines removed across the PR.

Author Lines Added Lines Removed
hdlmrell 8,235 304
Total 8,235 304

@coderabbitai

coderabbitai Bot commented Mar 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a building part registry, richer wall/opening APIs, interior-wall and interior-navigation systems (A* grid + navigator), roof/stair/decor builders, terrain clearing/flattening with retry, deferred networked-prefab linking, dual-material walls, and many builder API extensions and material-registration hooks.

Changes

Cohort / File(s) Summary
Building Core & Builder
Building/BuildingBuilder.cs
Adds Registry, GridCellSize, terrain flattening tied to foundation, CreateNavigationBuilder(), interior-wall APIs/layer, expanded AddWalls overloads (per-side openings, door+windows, wall-appearance overrides), prefab onCreated callback, part registration, stair metadata storage, and builder invalidation.
Part Registry & Config
Building/BuildingPartRegistry.cs, Building/Config/BuildingPart.cs, Building/Config/BuildingPalette.cs
New BuildingPartRegistry for post-build part/renderer lookup and material-swaps; BuildingPart enum added; palette gains InteriorWallMaterial/InteriorWallColor and WithInteriorWalls.
Interior Navigation Subsystem
Building/InteriorNavigator.cs, Building/InteriorNavigatorCore.cs, Building/InteriorPathGrid.cs, Building/NavigationBuilder.cs
New navigation types: NavDoorwayInfo, NavigationBuilder (build/remove, ramp colliders, carving obstacle), InteriorPathGrid (walkability grid, A* pathfinding, visualize/diagnose), and InteriorNavigatorCore (state machine, Harmony patches, NPC APIs).
Wall & Structural Builders
Building/Structural/WallBuilder.cs, Building/Structural/InteriorWallBuilder.cs, Building/Structural/DecorBuilder.cs, Building/Structural/RoofBuilder.cs
WallOpening expanded (multi-pane windows, offsets, door-with-windows), per-side WallAppearance, dual-material wall meshes; new InteriorWallBuilder with doorway tracking; DecorBuilder adds stairs (3 styles), door frames, corner trim, segmented base molding; RoofBuilder adds parapet & hip roofs.
Terrain & Environment
Building/Structural/TerrainClearer.cs, Building/Structural/TerrainFlattener.cs, Building/Structural/TerrainRetryBehaviour.cs
Adds TerrainClearer (configurable clearing passes), TerrainFlattener.FlattenUnder (Mono/IL2CPP-aware heightmap & detail clearing), and a retry queue/behaviour to defer terrain operations until terrain is available.
Prefab Placement & Networking
Building/Components/PrefabPlacer.cs, Building/Components/NetworkedPrefabLinker.cs, Core/PrefabRef.cs
PrefabRef adds inactive-networked instantiation; PrefabPlacer refactored with PlaceInternal and optional onReady callback, network-aware activation, and sliding-door customization; new NetworkedPrefabLinkerQueue/Behaviour to find/reparent replicated prefabs on clients.
Procedural Mesh & Generators
ProceduralMesh/CustomMeshBuilder.cs, ProceduralMesh/Generators/Primitives/DualMaterialBoxGenerator.cs, ProceduralMesh/ProceduralMeshBuilder.cs, ProceduralMesh/PrimitiveBuilder.cs
New internal DualMaterialBoxGenerator; MeshRenderers set renderingLayerMask = uint.MaxValue; CreatePointLight gains shadows and renderMode parameters.
Components & Small Adjustments
Building/Components/LightingBuilder.cs, Building/Components/NetworkedPrefabLinker.cs, Building/Components/PrefabPlacer.cs
Ceiling light Y lowered by 0.1; deferred prefab linking queue/behaviour added; PrefabPlacer networked placement flow centralized and safe activation after customization.
Utilities, Extensions & Constants
Building/BuildingUtilities.cs, Extensions/..., Utils/Constants.cs
Replaces SnapToGrid with ComputeGridCellSize; adds internal IsLivingEntity cache; switches child iteration to index-based loops; adds many constants (windows, roof, stairs, interior-nav, terrain, folders).
Builders Registration & Material APIs
Building/BuildingPartRegistry.cs, Building/Structural/*, Building/BuildingBuilder.cs
Many builders now register generated parts (exterior/interior walls, foundation, roof parts, stairs, trims, frames, moldings, prefabs) into BuildingPartRegistry for post-build queries and material swaps.
Project & Docs
docs/*, CODING_STANDARDS.md, S1MAPI.csproj
Docs extended for interior walls, navigation, terrain, roofs, registry, and networked prefab guidance; coding standards tightened on compile-time ScheduleOne references; csproj adds config-specific assembly names and terrain modules.

Sequence Diagram(s)

sequenceDiagram
    actor Builder as BuildingBuilder
    participant Terrain as TerrainFlattener
    participant NavBuilder as NavigationBuilder
    participant Grid as InteriorPathGrid
    participant NavCore as InteriorNavigatorCore
    participant NPC as NPCMovement

    Builder->>Terrain: FlattenUnder(targetY,...)
    Terrain->>Terrain: locate Terrain, modify heights, flush

    Builder->>NavBuilder: CreateNavigationBuilder()
    NavBuilder->>Grid: Construct grid from room & doorways
    Grid->>Grid: Generate walkability (walls, openings, physics probes)

    NavBuilder->>NavCore: Build core + attach navigator
    NavCore->>NavCore: install patches (intercept NPC.SetDestination)

    NPC->>NavCore: SetDestination(interiorTarget)
    NavCore->>Grid: FindPath(exterior->doorway)
    Grid-->>NavCore: waypoints
    NavCore->>NPC: approach doorway, lerp through (handle stairs)
    NavCore->>Grid: FindPath(doorway->interiorTarget)
    Grid-->>NavCore: interior waypoints
    NavCore->>NPC: traverse interior waypoints, trigger arrival
Loading
sequenceDiagram
    actor Client as FishNetClient
    participant Placer as PrefabPlacer
    participant Queue as NetworkedPrefabLinkerQueue
    participant Scene as SceneRoot
    participant Parent as BuildingParent

    Client->>Placer: Place(prefab, pos, networked=true, onReady)
    Placer->>Placer: InstantiateNetworkedInactive(...)
    Placer-->>Client: null (client cannot spawn)
    Placer->>Queue: Enqueue(prefabName, expectedPos, parent, localPose, onLinked)
    Queue->>Queue: Ensure behaviour, schedule polls

    Note over Scene: FishNet replicates instance later
    Queue->>Scene: Tick(): scan scene roots by prefab-name & pos
    Scene-->>Queue: Match found (pos/name)
    Queue->>Parent: Reparent instance, set local transform
    Queue->>Client: Invoke onLinked/onReady
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐇 I hopped through rafters, set each part in line,

Placed doors and panes, and trimmed each corner fine,
I flattened hills and carved a pathway new,
Registered bits so builders find their cue,
Now bunnies wander safely through rooms bright and true.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.43% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: interior wall systems, NPC navigation, terrain preparation, and post-build customization registry. It is directly related to the substantial feature additions throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ifBars ifBars self-assigned this Mar 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (6)
ProceduralMesh/PrimitiveBuilder.cs-273-275 (1)

273-275: ⚠️ Potential issue | 🟡 Minor

Behavioral change: shadow default changed from Soft to None affects all callers.

All three existing calls to CreatePointLight in LightingBuilder.cs (ceiling lights at line 70, point light at line 106, ambient lights at line 146) will now default to LightShadows.None instead of the previous hardcoded LightShadows.Soft. None of these callers pass the new shadows parameter, so they will silently adopt the new behavior.

For ceiling and ambient lights, None is appropriate and aligns with URP conventions. However, this is still a behavioral change that could affect visual output. Consider documenting this shift in migration notes or preserving the previous default (LightShadows.Soft) for backward compatibility, with None used explicitly where needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ProceduralMesh/PrimitiveBuilder.cs` around lines 273 - 275, The default for
the shadows parameter on CreatePointLight changed from LightShadows.Soft to
LightShadows.None, which silently alters behavior for all callers that don't
pass shadows (e.g., the ceiling/point/ambient lights in LightingBuilder); either
restore the original default to LightShadows.Soft in the CreatePointLight
signature to preserve backward compatibility, or explicitly update each caller
(the three CreatePointLight usages in LightingBuilder) to pass the intended
LightShadows value (None for ceiling/ambient, Soft where previously used), and
add a brief migration note documenting the change if you keep the new default.
Core/PrefabRef.cs-150-153 (1)

150-153: ⚠️ Potential issue | 🟡 Minor

XML doc contradicts actual parent timing.

Line 150 says parent is set before spawn, but the implementation parents after spawn (Lines 209-215). Please align the docs with current behavior.

Also applies to: 209-215

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/PrefabRef.cs` around lines 150 - 153, The XML doc for the PrefabRef
method that accepts (Transform parent, Vector3 localPosition, Quaternion
localRotation) incorrectly states the parent is set before spawn; update the
summary/param comment to reflect that the parent is assigned after the object is
spawned (matching the implementation where parent is applied post-spawn in the
PrefabRef method body around the block that handles parenting on lines 209-215).
Ensure the param description for "parent" and any return remarks explicitly say
the transform is parented after instantiation/network-spawn so docs match the
code.
Building/Config/BuildingPalette.cs-26-32 (1)

26-32: ⚠️ Potential issue | 🟡 Minor

These XML docs contradict each other.

Lines 26-28 say InteriorWallMaterial == null falls back to a single-material wall, but Lines 30-32 say InteriorWallColor is used in exactly that case. Generated docs will advertise both behaviors at once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Config/BuildingPalette.cs` around lines 26 - 32, The XML comments
for InteriorWallMaterial and InteriorWallColor conflict; reconcile them so they
describe the same fallback behavior. Update the comment on InteriorWallMaterial
and/or InteriorWallColor so they state a single consistent rule (e.g., "If
InteriorWallMaterial is null, exterior walls use InteriorWallColor; when
InteriorWallColor is null it defaults to WallColor" or the alternative chosen
behavior), referencing the properties InteriorWallMaterial, InteriorWallColor
and WallColor so both summaries match.
docs/examples.md-229-247 (1)

229-247: ⚠️ Potential issue | 🟡 Minor

Call out the server/host requirement before the networked: true placements.

This example is easy to copy into client-side startup code, but the networking overview later treats networked prefab spawning as server-authoritative. Add that precondition here so readers do not reproduce the multiplayer failure mode from the example itself.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/examples.md` around lines 229 - 247, The example calls
PrefabPlacer.Place with networked: true (e.g., PrefabPlacer.Place and the
Prefabs.MetalGlassDoor / Prefabs.ClassicalWoodenDoor usages) but doesn’t state
the server/host requirement; update the example text to explicitly require
running these networked placements only on the server/host (or when
isServer/isHost is true) and add a short guard comment above the two Place calls
indicating they must be executed on the authoritative server/host to ensure
FishNet server-authoritative spawning.
Building/Components/NetworkedPrefabLinker.cs-176-197 (1)

176-197: ⚠️ Potential issue | 🟡 Minor

Substring matching may link unintended objects.

The obj.name.Contains(req.PrefabName) check on Line 179 could match unrelated objects. For example, a request for "Door" would also match "DoorFrame", "SlidingDoors", or "BackDoor".

Consider using a more precise match (e.g., exact name or starts-with) and/or verifying the object has a NetworkObject component:

🛡️ Suggested stricter matching
-                if (!obj.name.Contains(req.PrefabName)) continue;
+                // Match by exact name or name(Clone) pattern from Instantiate
+                if (!obj.name.Equals(req.PrefabName) && 
+                    !obj.name.StartsWith(req.PrefabName + "(Clone)") &&
+                    !obj.name.StartsWith(req.PrefabName + " ("))
+                    continue;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Components/NetworkedPrefabLinker.cs` around lines 176 - 197, The
current substring match using obj.name.Contains(req.PrefabName) can link
unintended objects; update the matching in the loop to require a stricter check
(e.g., obj.name == req.PrefabName or obj.name.StartsWith(req.PrefabName,
StringComparison.Ordinal) depending on desired behavior) and additionally verify
the candidate has a NetworkObject component (e.g.,
TryGetComponent<NetworkObject>(out _)) before considering it for
PositionTolerance / bestDist logic so only valid networked prefabs are selected;
keep the existing distance/tolerance logic (PositionTolerance, bestMatch,
bestDist) otherwise.
Building/Structural/RoofBuilder.cs-178-186 (1)

178-186: ⚠️ Potential issue | 🟡 Minor

Apply the resolved slab material.

roofSlabMat is selected here but never assigned to the slab, so textured/shader overrides collapse to a plain colored box.

Possible fix
- PrimitiveBuilder.CreateBox("ParapetRoofSlab",
+ GameObject roofSlab = PrimitiveBuilder.CreateBox("ParapetRoofSlab",
     new Vector3(_roomSize.x / 2f, _roomSize.y + roofSlabThickness / 2f, _roomSize.z / 2f),
     new Vector3(_roomSize.x, roofSlabThickness, _roomSize.z),
     roofSlabMat.color, container.transform);
+ ApplyMaterial(roofSlab, roofSlabMat);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Structural/RoofBuilder.cs` around lines 178 - 186, The code computes
roofSlabMat but only passes roofSlabMat.color into PrimitiveBuilder.CreateBox,
losing material/texture/shader info; update the call that creates
"ParapetRoofSlab" to apply the full Material (roofSlabMat) instead of just its
color—either use the CreateBox overload that accepts a Material or capture the
returned GameObject from PrimitiveBuilder.CreateBox and assign roofSlabMat to
its Renderer.material (referencing roofSlabMat and the "ParapetRoofSlab"
CreateBox invocation).
🧹 Nitpick comments (10)
Building/Components/LightingBuilder.cs (1)

61-61: Consider extracting the ceiling offset to Constants.cs.

The change from 0.2f to 0.3f is reasonable and well-documented. Since the PR introduces centralized tuning constants in Constants.cs, this ceiling light offset (and potentially the 0.5f used in AddAmbientLighting) could be moved there for easier future adjustments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Components/LightingBuilder.cs` at line 61, Extract the magic ceiling
offset values into the centralized Constants.cs: replace the hardcoded 0.3f used
when computing yPos in LightingBuilder (the line setting float yPos =
_roomSize.y - 0.3f) with a descriptive constant (e.g., CeilingLightOffset) and
also move the 0.5f used in AddAmbientLighting into Constants.cs (e.g.,
AmbientLightHeightOffset); update references in LightingBuilder and
AddAmbientLighting to use those new constants and ensure names clearly reflect
purpose for easy tuning.
Building/Config/BuildingPart.cs (1)

7-28: Reserve 0 for an unspecified enum value.

default(BuildingPart) currently resolves to ExteriorWalls. Any uninitialized field, omitted serialized value, or defaulted array element will silently target a real category and can mis-register or repaint geometry. Add None = 0 and start the concrete parts at 1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Config/BuildingPart.cs` around lines 7 - 28, The BuildingPart enum
currently has no explicit zero value so default(BuildingPart) equals
ExteriorWalls; add an explicit None = 0 member and renumber the concrete parts
to start at 1 (e.g., set ExteriorWalls = 1 and increment the rest) so
uninitialized/omitted values map to None instead of a real part; update any
switch/default handling or serialization assumptions that rely on implicit
ordering if present (refer to the BuildingPart enum declaration and any
consumers that switch on BuildingPart).
ProceduralMesh/ProceduralMeshBuilder.cs (1)

220-221: Don't hard-code every generated mesh into all rendering layers.

This forces every procedural mesh to match every layer-masked light/decal, so callers lose rendering-layer isolation. Prefer a builder option for the mask, or keep Unity's default unless the caller explicitly opts into all layers. The same concern applies to CustomMeshBuilder.Build().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ProceduralMesh/ProceduralMeshBuilder.cs` around lines 220 - 221, The code
currently forces every generated mesh into all rendering layers by setting
MeshRenderer.renderingLayerMask = uint.MaxValue in ProceduralMeshBuilder (and
similarly in CustomMeshBuilder.Build()); change this to leave the renderer's
default mask unless the builder explicitly receives an option to override it
(e.g., expose a renderingLayerMask property/parameter on ProceduralMeshBuilder
and CustomMeshBuilder.Build), and only assign renderer.renderingLayerMask when
that option is provided by the caller so generated meshes retain Unity's default
layer isolation unless explicitly requested.
Building/Structural/TerrainRetryBehaviour.cs (1)

36-51: Closure captures may reference destroyed objects.

When TerrainFlattener.FlattenUnder enqueues a retry, the lambda captures buildingRoot. If the building GameObject is destroyed before the retry executes, the captured reference becomes invalid (Unity returns null for destroyed objects, but the closure still holds it).

Consider adding a null-check on target at the start of Tick or within the queued operation itself to gracefully skip retries for destroyed buildings.

🛡️ Suggested defensive check in Tick
 internal static bool Tick(int id, string buildingName)
 {
     if (!States.TryGetValue(id, out var state) || state.Ops.Count == 0)
     {
         States.Remove(id);
         return false;
     }
+
+    // Early exit if the building was destroyed
+    // (buildingName will be null or empty for destroyed objects)
+    if (string.IsNullOrEmpty(buildingName))
+    {
+        DebugLog.Info("[TerrainRetry] Building destroyed — clearing pending ops.");
+        States.Remove(id);
+        return false;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Structural/TerrainRetryBehaviour.cs` around lines 36 - 51, The
queued lambda can hold a reference to a destroyed GameObject
(target/buildingRoot), so update TerrainRetryBehaviour to skip retries when the
target has been destroyed: in the Tick method (or just before invoking each
queued operation stored in RetryState.Ops) check the associated GameObject
reference (the same target passed into Enqueue / found via
TerrainRetryBehaviour.EnsureOn) using UnityEngine.Object null semantics (target
== null) and remove/skip any operations for destroyed targets instead of
invoking them; ensure you adjust RetryState handling so Ops count and States
cleanup reflect skipped/dropped operations.
Building/NavigationBuilder.cs (1)

306-333: Consider disabling carving when building is deactivated.

The NavMeshObstacle with carving=true will continue carving the NavMesh even if the building GameObject is deactivated (only destroyed objects stop carving). If buildings can be toggled inactive, this could leave phantom "holes" in the NavMesh.

If building deactivation is a supported use case, consider adding an OnDisable/OnEnable hook in InteriorNavigator to toggle NavMeshObstacle.carving.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/NavigationBuilder.cs` around lines 306 - 333, The NavMeshObstacle
created in PlaceFootprintCarvingObstacle uses carving=true and will keep carving
when its GameObject is deactivated; add logic to toggle obstacle.carving on
enable/disable so deactivated buildings don't leave NavMesh holes. Specifically,
update InteriorNavigator (or the component that manages _carvingObstacles
created by PlaceFootprintCarvingObstacle) to implement OnDisable and OnEnable
handlers that iterate _carvingObstacles (the GameObjects returned from
PlaceFootprintCarvingObstacle), get their NavMeshObstacle components, and set
obstacle.carving = false in OnDisable and obstacle.carving = true in OnEnable
(guarding for nulls), ensuring carving is restored only for active buildings.
Building/Components/NetworkedPrefabLinker.cs (1)

199-228: Consider checking if the matched object is already parented.

If a matched object is already parented to another transform (e.g., a different building instance), reparenting it could cause unexpected behavior. A simple guard would prevent stealing objects from other hierarchies:

🛡️ Suggested parent check
             if (bestMatch != null)
             {
+                // Don't steal objects already parented elsewhere
+                if (bestMatch.transform.parent != null)
+                {
+                    DebugLog.Warning($"[PrefabLinker] '{bestMatch.name}' already has parent " +
+                                      $"'{bestMatch.transform.parent.name}' — skipping.");
+                    return false;
+                }
+
                 if (bestDist > PositionTolerance)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Components/NetworkedPrefabLinker.cs` around lines 199 - 228,
bestMatch may already be parented to a different Transform, so add a guard
before reparenting: check bestMatch.transform.parent (or
bestMatch.transform.IsChildOf(req.Parent)) and if it is non-null and not the
same as req.Parent, skip linking (log an Info/Warning and return false or
continue searching) to avoid stealing objects from other hierarchies; place this
check just before bestMatch.transform.SetParent(req.Parent) in the matching
block that uses bestMatch, req.Parent, PositionTolerance, attempts and still
invoke req.OnLinked only when you actually perform the reparenting.
Building/Components/PrefabPlacer.cs (1)

64-80: Consider simplifying callback composition.

The current if-else chain for combining enableComponents and onReady is verbose. A more concise approach:

♻️ Simplified callback composition
-            Action<GameObject>? combined = null;
-            if (enableComponents && onReady != null)
-            {
-                combined = (go) => { go.EnableAllComponents(recursive: true); onReady(go); };
-            }
-            else if (enableComponents)
-            {
-                combined = (go) => go.EnableAllComponents(recursive: true);
-            }
-            else
-            {
-                combined = onReady;
-            }
+            Action<GameObject>? combined = enableComponents
+                ? (go) => { go.EnableAllComponents(recursive: true); onReady?.Invoke(go); }
+                : onReady;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Components/PrefabPlacer.cs` around lines 64 - 80, The Place method
builds a combined callback with verbose if/else; simplify by creating a single
lambda assigned to combined that, when invoked, calls
go.EnableAllComponents(recursive: true) if enableComponents is true and then
calls onReady(go) if onReady is not null, otherwise set combined = onReady when
enableComponents is false, then pass combined into PlaceInternal; update
references to Place, PlaceInternal and EnableAllComponents accordingly so
behavior is identical but code is more concise.
Building/InteriorPathGrid.cs (2)

440-447: DuplicateKeyComparer trick is clever but subtle.

Returning 1 for equal keys allows duplicate f-scores but breaks the IComparer contract (transitivity). This works for SortedList but could cause issues if the comparer is reused elsewhere.

Consider adding a comment explaining this intentional contract violation:

📝 Suggested clarifying comment
         /// <summary>
         /// Comparer that allows duplicate keys in SortedList.
+        /// WARNING: Violates IComparer contract by never returning 0.
+        /// Only safe for use with SortedList as a pseudo priority queue.
         /// </summary>
         private sealed class DuplicateKeyComparer : IComparer<float>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/InteriorPathGrid.cs` around lines 440 - 447, DuplicateKeyComparer
currently returns 1 for equal floats to allow duplicate f-scores but
intentionally violates IComparer transitivity; update the Compare method
(DuplicateKeyComparer.Compare) with a clear comment explaining that returning 1
is a deliberate workaround to allow duplicate keys in the SortedList used by
InteriorPathGrid and that this comparer must not be reused elsewhere (or
consider replacing with a different data structure if broader use is needed).
Keep the class sealed/private scope note and mention the specific behavior
(treat equal as greater) and the risk (breaks IComparer contract/transitivity)
so future maintainers understand the intentional tradeoff.

366-414: SortedList as priority queue has O(n) removal cost.

SortedList.RemoveAt(0) is O(n) because it shifts all remaining elements. For pathfinding on larger grids, this could become a bottleneck. The comment on Line 366 acknowledges this is "fine for few hundred cells."

If grids grow larger, consider switching to a proper min-heap (e.g., PriorityQueue<TElement, TPriority> in .NET 6+, or a custom binary heap for earlier targets).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/InteriorPathGrid.cs` around lines 366 - 414, The current A* uses a
SortedList named open with DuplicateKeyComparer and calls open.RemoveAt(0) which
is O(n); replace this with a proper min-heap (preferably
System.Collections.Generic.PriorityQueue<int, float> when targeting .NET 6+) or
a small custom binary heap class: create a PriorityQueue<int,float> open, use
open.Enqueue(startIdx, fScore[startIdx]) instead of open.Add(...), use
open.TryDequeue(out int currentIdx, out float _ ) (or Dequeue) to pop the
lowest-f node, keep the existing closed[] check immediately after dequeuing, and
when updating a neighbor call open.Enqueue(neighborIdx, fScore[neighborIdx]) (or
your heap.Push) instead of open.Add; remove the DuplicateKeyComparer and any
logic dependent on SortedList ordering. Ensure all references to
open.RemoveAt(0) and open.Add(...) are replaced and that the algorithm still
checks closed[neighborIdx] before processing.
docs/building.md (1)

176-183: Consider adding error handling guidance for NavigationBuilder.

The example shows navBuilder.Build() but doesn't mention what happens if the building isn't positioned yet or if Build() is called multiple times. The NavigationBuilder code logs a warning for double-builds. Consider adding a brief note:

> **Note:** Call `Build()` only once after the building is positioned. 
> Use `Rebuild()` if interior objects change later.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/building.md` around lines 176 - 183, Add a short note in the docs near
the NavigationBuilder example clarifying lifecycle and error handling: explain
that NavigationBuilder is obtained via
buildingBuilder.CreateNavigationBuilder(), that navBuilder.Build() should be
called once after the building is positioned (calling Build() multiple times
triggers a logged warning), and that callers should use navBuilder.Rebuild()
when interior objects change later instead of repeatedly calling Build().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Building/BuildingBuilder.cs`:
- Around line 150-152: GetDecorBuilder(...) returns a memoized DecorBuilder
whose constructor captures the palette, so calling AddFloor(...) or
AddCeiling(...) with a per-call palette override produces stale renders; change
BuildingBuilder to only apply the palette override on the first decor build and
ignore/remove per-call palette arguments for subsequent calls: locate
GetDecorBuilder, the calls to AddFloor(_config.FloorThickness) and
AddCeiling(...) and the DecorBuilder constructor usage, ensure the palette is
set once when the DecorBuilder is created and remove or bypass any palette
cloning/override logic on later AddFloor/AddCeiling invocations so subsequent
builds use the already-initialized DecorBuilder's palette.
- Around line 360-384: CreateNavigationBuilder can produce a doorway list with
zero exterior entries which later breaks NavigationBuilder; before constructing
NavigationBuilder, check the doorways list for at least one exterior doorway
(i.e., an entry where isInterior is false—inspect NavDoorwayInfo's
property/field that represents interior vs exterior or track the isInterior
constructor arg), and if none exist, fail early (throw a clear
InvalidOperationException or return an error) instead of constructing
NavigationBuilder; update CreateNavigationBuilder to perform this guard and
include a descriptive message referencing CreateNavigationBuilder and
NavigationBuilder.
- Around line 564-579: AddStairs currently only stores (wall, foundationHeight,
width, lateralOffset) so ComputeStairBasePosition reconstructs stair run with
defaults and ignores stepDepth, maxStepHeight, gap, flushWithFloor and style;
change the stored stair record (_stairs) to include the full stair specification
used in GetDecorBuilder().AddStairs (e.g., stepDepth, maxStepHeight,
color/material identifiers, style, flushWithFloor, gap) or create a
StairSpec/DTO and push that into _stairs, then update ComputeStairBasePosition
to read the real stair parameters from that StairSpec and compute the base/run
accordingly so navigation/doorway placement matches actual stair geometry.
Ensure any registry registration (_registry.Register(BuildingPart.Stairs,
stairs)) still happens but does not discard the full spec.

In `@Building/BuildingPartRegistry.cs`:
- Around line 205-208: The code currently checks only that submeshIndex <
mats.Length before assigning mats[submeshIndex] = material and updating
renderers[i].materials; add a lower-bound check so you only index when
submeshIndex >= 0 && submeshIndex < mats.Length (e.g. skip the assignment/update
when out of range) to prevent negative-index exceptions; locate the logic around
submeshIndex, mats, material and renderers[i].materials in BuildingPartRegistry
and apply this guard.

In `@Building/BuildingUtilities.cs`:
- Around line 143-149: ComputeGridCellSize currently returns a cell size that
may not evenly divide roomX or roomZ for non-square rooms (e.g., 10x7),
violating the implied contract; change the API to return both the computed cell
size and the integer cell counts so callers can reconstruct exact tiling. Update
the ComputeGridCellSize method signature to return a tuple or a small struct
(e.g., (float cellSize, int nx, int nz) or GridCellResult) or add out parameters
for nx and nz, compute nx = Mathf.Max(1, Mathf.CeilToInt(roomX / target)) and nz
= Mathf.Max(1, Mathf.CeilToInt(roomZ / target)), compute cellSize as
Mathf.Min(roomX / nx, roomZ / nz), and ensure callers of ComputeGridCellSize
(and any related doc/comments) are updated to use the returned counts instead of
recomputing them.
- Around line 158-170: The IsLivingEntity method currently treats any root with
an Animator as "living," which is too broad; modify IsLivingEntity to use a
stricter character test (e.g., check for game-specific character components such
as NPCMovement, CharacterController, PlayerCharacter, or other explicit
character marker components) instead of relying on Animator, and add an optional
override hook so callers can supply a predicate (or use an existing
ClearingOptions.Filter) to decide "living" status; update references to
livingRoots and staticRoots logic to only add rootId to livingRoots when the
stricter component check or caller predicate returns true, otherwise add to
staticRoots.

In `@Building/InteriorNavigator.cs`:
- Around line 20-29: Wrap calls to _core.Update() in Update() and
_core.Cleanup() in OnDestroy() with try/catch blocks that catch exceptions, log
the exception (including context) and then clear/null the _core reference and
disable further processing so a single failure doesn't repeat every frame;
specifically modify Update() to try { _core.Update(); } catch (Exception ex) {
processLogger?.Error/Debug with context; _core = null; } and modify OnDestroy()
to try { _core.Cleanup(); } catch (Exception ex) { log the error; } finally {
_core = null; } so teardown always completes and no repeated exceptions occur.

In `@Building/InteriorNavigatorCore.cs`:
- Around line 672-692: ReleaseAllNPCs currently omits restoring the
TrackedNPC.HasDestination flag (which BeginDoorwayEntry clears and ReleaseNPC
restores), causing NPC movement loops to remain disabled after bulk release;
update ReleaseAllNPCs (in the loop over _tracked) to set
kvp.Value.HasDestination = true (or use the same restore logic as ReleaseNPC)
for NPCs being released (e.g., before calling EnableAgent and re-enabling
colliders), ensuring NPCs removed in Cleanup regain their destination state.
- Around line 374-395: The new branch unconditionally registers the TrackedNPC
(from CreateTrackedNPC) into nav._tracked and _globallyManaged and returns false
even when tracked.Agent is null/disabled/off-navmesh, which deadlocks
UpdateApproaching; change the logic in the block that sets
tracked.TargetDoorway/State/etc. so you only add to nav._tracked and
_globallyManaged and return false when tracked.Agent != null &&
tracked.Agent.enabled && tracked.Agent.isOnNavMesh (i.e. the same condition used
for SetDestination); otherwise skip adding the tracked NPC (or let the original
move proceed) so the original movement is not suppressed when the agent is
unavailable.
- Around line 1113-1117: The double null check around data.ChaseTarget is
ineffective because both checks use Unity's overloaded null operator (so a
destroyed Transform makes the outer != null false and the inner == null never
runs); modify the logic in the method containing data.ChaseTarget to first cache
the raw reference (e.g., var chase = data.ChaseTarget) and then use
System.Object.ReferenceEquals(chase, null) for the existence test or explicitly
check for destroyed via chase == null vs ReferenceEquals to distinguish
destroyed instances, and call BeginExit(npc, data) when the cached reference is
destroyed or becomes null; update any related conditionals so BeginExit is
invoked correctly when the target was destroyed.

In `@Building/Structural/DecorBuilder.cs`:
- Around line 681-683: The calculation for tread vertical position uses "+
treadThickness / 2f" which places treads one full thickness too high; update the
computation of the variable treadY (and the same pattern at the other
occurrences around the open-stringer/closed-riser code) to subtract half the
tread thickness from the intended top surface instead (i.e., use "-
treadThickness / 2f" relative to the step surface expression that uses
foundationHeight, height and gap) so treads sit correctly on top without
clipping.

In `@Building/Structural/TerrainClearer.cs`:
- Around line 159-160: The current logic computes an axis-aligned Bounds via
ComputeWorldBounds(buildingRoot.transform, roomSize) and then calls
ClearArea(bounds, opts) which only tests t.position, causing over-clear for
rotated buildings and missed large props whose pivots lie outside the AABB.
Update the clear routine to perform containment tests in building-local space
(transform objects into buildingRoot local coordinates) and test against each
renderer.bounds (and/or the GameObject root bounds) instead of only t.position;
modify ClearArea (and the other similar block around 299-307) to check
renderer.bounds.Contains for multiple sample points or use Bounds.Intersects on
the transformed renderer bounds to ensure footprint-accurate removal while
respecting building rotation and object extents.

In `@Building/Structural/WallBuilder.cs`:
- Around line 355-356: Clamp the opening.Offset before computing
leftWidth/rightWidth so a large offset cannot make a side width negative:
compute halfMargin = (wallWidth - doorWidth)/2f, then limit offset to
Mathf.Sign(offset) * Mathf.Min(Mathf.Abs(opening.Offset), halfMargin) (or use
Mathf.Clamp(opening.Offset, -halfMargin, halfMargin)) and then compute
leftWidth/rightWidth from that clamped value; apply the same clamping logic to
the analogous calculation around lines 577-580 so no side width becomes negative
and subsequent geometry generation cannot extend past wall bounds.
- Around line 763-767: The current guard in CreateWallSegment only calls
CreateDualMaterialBox when _currentInteriorMaterial is non-null, which ignores
interior color-only overrides; change the condition in CreateWallSegment to call
CreateDualMaterialBox when either _currentInteriorMaterial is non-null OR an
interior color override exists (e.g. _currentInteriorColor is not the
default/empty value or a boolean like _hasInteriorColorOverride is true) so
CreateDualMaterialBox can synthesize the interior material from
_currentInteriorColor even when no material override is provided.

In `@Core/PrefabRef.cs`:
- Around line 203-220: The code currently silently skips spawn when
instance.GetComponent<NetworkObject>() returns null; update InstantiateNetworked
to fail fast: after var netObj = instance.GetComponent<NetworkObject>(); if
netObj is null then clean up the created instance (Destroy(instance) or
DestroyImmediate(instance) as appropriate) and throw a clear
InvalidOperationException (or ArgumentException) stating that the prefab passed
to InstantiateNetworked must contain a NetworkObject to avoid client/server
desync; reference InstantiateNetworked, NetworkObject, and
nm.ServerManager.Spawn in the message so callers can locate the issue.

In `@ProceduralMesh/Generators/Organic/OrganicShapeGenerator.cs`:
- Line 19: The XML doc example uses invalid C# generic syntax with curly braces;
update the documentation for the protected override method GenerateGeometry to
use escaped angle brackets: replace occurrences of "List{Vector3}" and
"List{int}" in the XML comment with "List&lt;Vector3&gt;" and "List&lt;int&gt;"
so the shown signature "protected override void
GenerateGeometry(List&lt;Vector3&gt; vertices, List&lt;int&gt; triangles)" is
valid and safe to copy.

---

Minor comments:
In `@Building/Components/NetworkedPrefabLinker.cs`:
- Around line 176-197: The current substring match using
obj.name.Contains(req.PrefabName) can link unintended objects; update the
matching in the loop to require a stricter check (e.g., obj.name ==
req.PrefabName or obj.name.StartsWith(req.PrefabName, StringComparison.Ordinal)
depending on desired behavior) and additionally verify the candidate has a
NetworkObject component (e.g., TryGetComponent<NetworkObject>(out _)) before
considering it for PositionTolerance / bestDist logic so only valid networked
prefabs are selected; keep the existing distance/tolerance logic
(PositionTolerance, bestMatch, bestDist) otherwise.

In `@Building/Config/BuildingPalette.cs`:
- Around line 26-32: The XML comments for InteriorWallMaterial and
InteriorWallColor conflict; reconcile them so they describe the same fallback
behavior. Update the comment on InteriorWallMaterial and/or InteriorWallColor so
they state a single consistent rule (e.g., "If InteriorWallMaterial is null,
exterior walls use InteriorWallColor; when InteriorWallColor is null it defaults
to WallColor" or the alternative chosen behavior), referencing the properties
InteriorWallMaterial, InteriorWallColor and WallColor so both summaries match.

In `@Building/Structural/RoofBuilder.cs`:
- Around line 178-186: The code computes roofSlabMat but only passes
roofSlabMat.color into PrimitiveBuilder.CreateBox, losing
material/texture/shader info; update the call that creates "ParapetRoofSlab" to
apply the full Material (roofSlabMat) instead of just its color—either use the
CreateBox overload that accepts a Material or capture the returned GameObject
from PrimitiveBuilder.CreateBox and assign roofSlabMat to its Renderer.material
(referencing roofSlabMat and the "ParapetRoofSlab" CreateBox invocation).

In `@Core/PrefabRef.cs`:
- Around line 150-153: The XML doc for the PrefabRef method that accepts
(Transform parent, Vector3 localPosition, Quaternion localRotation) incorrectly
states the parent is set before spawn; update the summary/param comment to
reflect that the parent is assigned after the object is spawned (matching the
implementation where parent is applied post-spawn in the PrefabRef method body
around the block that handles parenting on lines 209-215). Ensure the param
description for "parent" and any return remarks explicitly say the transform is
parented after instantiation/network-spawn so docs match the code.

In `@docs/examples.md`:
- Around line 229-247: The example calls PrefabPlacer.Place with networked: true
(e.g., PrefabPlacer.Place and the Prefabs.MetalGlassDoor /
Prefabs.ClassicalWoodenDoor usages) but doesn’t state the server/host
requirement; update the example text to explicitly require running these
networked placements only on the server/host (or when isServer/isHost is true)
and add a short guard comment above the two Place calls indicating they must be
executed on the authoritative server/host to ensure FishNet server-authoritative
spawning.

In `@ProceduralMesh/PrimitiveBuilder.cs`:
- Around line 273-275: The default for the shadows parameter on CreatePointLight
changed from LightShadows.Soft to LightShadows.None, which silently alters
behavior for all callers that don't pass shadows (e.g., the
ceiling/point/ambient lights in LightingBuilder); either restore the original
default to LightShadows.Soft in the CreatePointLight signature to preserve
backward compatibility, or explicitly update each caller (the three
CreatePointLight usages in LightingBuilder) to pass the intended LightShadows
value (None for ceiling/ambient, Soft where previously used), and add a brief
migration note documenting the change if you keep the new default.

---

Nitpick comments:
In `@Building/Components/LightingBuilder.cs`:
- Line 61: Extract the magic ceiling offset values into the centralized
Constants.cs: replace the hardcoded 0.3f used when computing yPos in
LightingBuilder (the line setting float yPos = _roomSize.y - 0.3f) with a
descriptive constant (e.g., CeilingLightOffset) and also move the 0.5f used in
AddAmbientLighting into Constants.cs (e.g., AmbientLightHeightOffset); update
references in LightingBuilder and AddAmbientLighting to use those new constants
and ensure names clearly reflect purpose for easy tuning.

In `@Building/Components/NetworkedPrefabLinker.cs`:
- Around line 199-228: bestMatch may already be parented to a different
Transform, so add a guard before reparenting: check bestMatch.transform.parent
(or bestMatch.transform.IsChildOf(req.Parent)) and if it is non-null and not the
same as req.Parent, skip linking (log an Info/Warning and return false or
continue searching) to avoid stealing objects from other hierarchies; place this
check just before bestMatch.transform.SetParent(req.Parent) in the matching
block that uses bestMatch, req.Parent, PositionTolerance, attempts and still
invoke req.OnLinked only when you actually perform the reparenting.

In `@Building/Components/PrefabPlacer.cs`:
- Around line 64-80: The Place method builds a combined callback with verbose
if/else; simplify by creating a single lambda assigned to combined that, when
invoked, calls go.EnableAllComponents(recursive: true) if enableComponents is
true and then calls onReady(go) if onReady is not null, otherwise set combined =
onReady when enableComponents is false, then pass combined into PlaceInternal;
update references to Place, PlaceInternal and EnableAllComponents accordingly so
behavior is identical but code is more concise.

In `@Building/Config/BuildingPart.cs`:
- Around line 7-28: The BuildingPart enum currently has no explicit zero value
so default(BuildingPart) equals ExteriorWalls; add an explicit None = 0 member
and renumber the concrete parts to start at 1 (e.g., set ExteriorWalls = 1 and
increment the rest) so uninitialized/omitted values map to None instead of a
real part; update any switch/default handling or serialization assumptions that
rely on implicit ordering if present (refer to the BuildingPart enum declaration
and any consumers that switch on BuildingPart).

In `@Building/InteriorPathGrid.cs`:
- Around line 440-447: DuplicateKeyComparer currently returns 1 for equal floats
to allow duplicate f-scores but intentionally violates IComparer transitivity;
update the Compare method (DuplicateKeyComparer.Compare) with a clear comment
explaining that returning 1 is a deliberate workaround to allow duplicate keys
in the SortedList used by InteriorPathGrid and that this comparer must not be
reused elsewhere (or consider replacing with a different data structure if
broader use is needed). Keep the class sealed/private scope note and mention the
specific behavior (treat equal as greater) and the risk (breaks IComparer
contract/transitivity) so future maintainers understand the intentional
tradeoff.
- Around line 366-414: The current A* uses a SortedList named open with
DuplicateKeyComparer and calls open.RemoveAt(0) which is O(n); replace this with
a proper min-heap (preferably System.Collections.Generic.PriorityQueue<int,
float> when targeting .NET 6+) or a small custom binary heap class: create a
PriorityQueue<int,float> open, use open.Enqueue(startIdx, fScore[startIdx])
instead of open.Add(...), use open.TryDequeue(out int currentIdx, out float _ )
(or Dequeue) to pop the lowest-f node, keep the existing closed[] check
immediately after dequeuing, and when updating a neighbor call
open.Enqueue(neighborIdx, fScore[neighborIdx]) (or your heap.Push) instead of
open.Add; remove the DuplicateKeyComparer and any logic dependent on SortedList
ordering. Ensure all references to open.RemoveAt(0) and open.Add(...) are
replaced and that the algorithm still checks closed[neighborIdx] before
processing.

In `@Building/NavigationBuilder.cs`:
- Around line 306-333: The NavMeshObstacle created in
PlaceFootprintCarvingObstacle uses carving=true and will keep carving when its
GameObject is deactivated; add logic to toggle obstacle.carving on
enable/disable so deactivated buildings don't leave NavMesh holes. Specifically,
update InteriorNavigator (or the component that manages _carvingObstacles
created by PlaceFootprintCarvingObstacle) to implement OnDisable and OnEnable
handlers that iterate _carvingObstacles (the GameObjects returned from
PlaceFootprintCarvingObstacle), get their NavMeshObstacle components, and set
obstacle.carving = false in OnDisable and obstacle.carving = true in OnEnable
(guarding for nulls), ensuring carving is restored only for active buildings.

In `@Building/Structural/TerrainRetryBehaviour.cs`:
- Around line 36-51: The queued lambda can hold a reference to a destroyed
GameObject (target/buildingRoot), so update TerrainRetryBehaviour to skip
retries when the target has been destroyed: in the Tick method (or just before
invoking each queued operation stored in RetryState.Ops) check the associated
GameObject reference (the same target passed into Enqueue / found via
TerrainRetryBehaviour.EnsureOn) using UnityEngine.Object null semantics (target
== null) and remove/skip any operations for destroyed targets instead of
invoking them; ensure you adjust RetryState handling so Ops count and States
cleanup reflect skipped/dropped operations.

In `@docs/building.md`:
- Around line 176-183: Add a short note in the docs near the NavigationBuilder
example clarifying lifecycle and error handling: explain that NavigationBuilder
is obtained via buildingBuilder.CreateNavigationBuilder(), that
navBuilder.Build() should be called once after the building is positioned
(calling Build() multiple times triggers a logged warning), and that callers
should use navBuilder.Rebuild() when interior objects change later instead of
repeatedly calling Build().

In `@ProceduralMesh/ProceduralMeshBuilder.cs`:
- Around line 220-221: The code currently forces every generated mesh into all
rendering layers by setting MeshRenderer.renderingLayerMask = uint.MaxValue in
ProceduralMeshBuilder (and similarly in CustomMeshBuilder.Build()); change this
to leave the renderer's default mask unless the builder explicitly receives an
option to override it (e.g., expose a renderingLayerMask property/parameter on
ProceduralMeshBuilder and CustomMeshBuilder.Build), and only assign
renderer.renderingLayerMask when that option is provided by the caller so
generated meshes retain Unity's default layer isolation unless explicitly
requested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1bc1b36-d02f-496b-874b-79570686074e

📥 Commits

Reviewing files that changed from the base of the PR and between 545aef7 and c4b9eb3.

📒 Files selected for processing (34)
  • Building/BuildingBuilder.cs
  • Building/BuildingPartRegistry.cs
  • Building/BuildingUtilities.cs
  • Building/Components/LightingBuilder.cs
  • Building/Components/NetworkedPrefabLinker.cs
  • Building/Components/PrefabPlacer.cs
  • Building/Config/BuildingPalette.cs
  • Building/Config/BuildingPart.cs
  • Building/InteriorNavigator.cs
  • Building/InteriorNavigatorCore.cs
  • Building/InteriorPathGrid.cs
  • Building/NavigationBuilder.cs
  • Building/Structural/DecorBuilder.cs
  • Building/Structural/InteriorWallBuilder.cs
  • Building/Structural/RoofBuilder.cs
  • Building/Structural/TerrainClearer.cs
  • Building/Structural/TerrainFlattener.cs
  • Building/Structural/TerrainRetryBehaviour.cs
  • Building/Structural/WallAppearance.cs
  • Building/Structural/WallBuilder.cs
  • CODING_STANDARDS.md
  • Core/PrefabRef.cs
  • Extensions/GameObjectExtensions.cs
  • Extensions/TransformExtensions.cs
  • ProceduralMesh/CustomMeshBuilder.cs
  • ProceduralMesh/Generators/Organic/OrganicShapeGenerator.cs
  • ProceduralMesh/Generators/Primitives/DualMaterialBoxGenerator.cs
  • ProceduralMesh/PrimitiveBuilder.cs
  • ProceduralMesh/ProceduralMeshBuilder.cs
  • S1MAPI.csproj
  • Utils/Constants.cs
  • docs/api-overview.md
  • docs/building.md
  • docs/examples.md

Comment thread Building/BuildingBuilder.cs Outdated
Comment thread Building/BuildingBuilder.cs
Comment thread Building/BuildingBuilder.cs
Comment thread Building/BuildingPartRegistry.cs Outdated
Comment thread Building/BuildingUtilities.cs
Comment on lines +159 to +160
Bounds bounds = ComputeWorldBounds(buildingRoot.transform, roomSize);
ClearArea(bounds, opts);

@coderabbitai coderabbitai Bot Mar 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This containment logic is not actually footprint-accurate.

The building is reduced to an axis-aligned Bounds, and the object scan only tests t.position. For rotated buildings that over-clears objects outside the real footprint, while large props whose pivot sits outside the box can survive and clip into the building. Use building-local containment plus renderer bounds/root bounds for the removal tests.

Also applies to: 299-307

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/Structural/TerrainClearer.cs` around lines 159 - 160, The current
logic computes an axis-aligned Bounds via
ComputeWorldBounds(buildingRoot.transform, roomSize) and then calls
ClearArea(bounds, opts) which only tests t.position, causing over-clear for
rotated buildings and missed large props whose pivots lie outside the AABB.
Update the clear routine to perform containment tests in building-local space
(transform objects into buildingRoot local coordinates) and test against each
renderer.bounds (and/or the GameObject root bounds) instead of only t.position;
modify ClearArea (and the other similar block around 299-307) to check
renderer.bounds.Contains for multiple sample points or use Bounds.Intersects on
the transformed renderer bounds to ensure footprint-accurate removal while
respecting building rotation and object extents.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AABB does account for rotation. The tradeoff is that for rotated buildings, the box is slightly larger than the footprint, so extra space is cleared in the corners. I find this acceptable and don't consider it major. Leave unresolved for maintainer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hdlmrell, understood — ComputeWorldBounds projects the actual building corners into world space, so the AABB is already rotation-aware. The slight over-clear in diagonal corners is a conscious conservative tradeoff, and ClearingOptions.Filter / ProtectedKeywords remain the escape hatch for anything that shouldn't be removed. I'll record this for future reviews.

(ᓀ‸ᓂ)


✏️ Learnings added
Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/Structural/TerrainClearer.cs:159-160
Timestamp: 2026-03-29T18:40:55.825Z
Learning: In `Building/Structural/TerrainClearer.cs` (`S1MAPI` project), `ClearAroundBuilding` uses `ComputeWorldBounds` which projects the building's actual corners into world space, producing a rotation-aware AABB. For rotated buildings the AABB is a conservative envelope that slightly over-clears the diagonal corners; this tradeoff is intentional and acceptable by design. Do not flag the bounds computation or the t.position containment test as footprint-inaccurate in future reviews. `ClearingOptions.Filter` and `ProtectedKeywords` are the intended escape hatches for edge cases.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/BuildingUtilities.cs:158-170
Timestamp: 2026-03-28T16:13:06.378Z
Learning: In `Building/BuildingUtilities.cs` (`S1MAPI` project), `IsLivingEntity` uses `Animator` as the proxy for living entities intentionally. In practice, terrain objects in ScheduleOne do not have `Animator` components, so false positives are not a real concern. `ClearingOptions.Filter` is the intended escape hatch for any edge cases. The preferred alternative (if ever needed) is `NavMeshAgent || CharacterController`, which are Unity built-ins compatible with S1MAPI's policy of avoiding ScheduleOne-specific type dependencies.

Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/BuildingUtilities.cs:143-149
Timestamp: 2026-03-28T13:59:01.859Z
Learning: In `Building/BuildingUtilities.cs` (`S1MAPI` project), `ComputeGridCellSize(float roomX, float roomZ)` intentionally returns a conservative square cell size that may not evenly divide both room axes. Partial-width cells at the far edge are acceptable by design. Do not flag this as a contract violation in future reviews.

Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/BuildingUtilities.cs:158-170
Timestamp: 2026-03-28T16:03:16.357Z
Learning: In `Building/BuildingUtilities.cs` (`S1MAPI` project), `IsLivingEntity` currently uses `Animator` as the proxy for living entities (NPCs/players), which is acknowledged as too broad (animated props would match). The preferred fix is to check for `NavMeshAgent || CharacterController` instead — NPCs have `NavMeshAgent` (confirmed via `InteriorNavigatorCore` TrackedNPC.Agent), and players have `CharacterController`. An optional static `Func<Transform, bool>? LivingEntityOverride` delegate on `BuildingUtilities` can be added as an escape hatch for game-specific component checks without breaking consumers.

Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/InteriorNavigatorCore.cs:375-396
Timestamp: 2026-03-28T16:32:52.505Z
Learning: In `Building/InteriorNavigatorCore.cs` (`S1MAPI` project), `SetDestinationPrefix` intentionally adds a `TrackedNPC` to `_tracked`/`_globallyManaged` and returns `false` (blocks the original) even when the `NavMeshAgent` is null/disabled/off-navmesh. The null/disabled agent case is handled by a 12-second timeout in `UpdateApproaching`. Returning `true` (allowing the original) when the agent is unavailable would route the NPC into a carved-out NavMesh interior where it cannot navigate, which is a worse outcome. Do not suggest adding an early-return guard for unavailable agents in `SetDestinationPrefix`.

Comment thread Building/Structural/WallBuilder.cs Outdated
Comment thread Building/Structural/WallBuilder.cs
Comment thread Core/PrefabRef.cs
Comment on lines +203 to 220
var netObj = instance.GetComponent<NetworkObject>();
if (netObj != null)
{
nm.ServerManager.Spawn(netObj);
}

// Parent after spawn (local operation, not replicated)
if (parent != null)
{
instance.transform.SetParent(parent);
instance.transform.localPosition = localPosition;
instance.transform.localRotation = localRotation;
}

if (activate)
instance.SetActive(true);

return instance;

@coderabbitai coderabbitai Bot Mar 27, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "PrefabRef.cs" -type f

Repository: ifBars/S1MAPI

Length of output: 76


🏁 Script executed:

head -n 250 Core/PrefabRef.cs | tail -n 100

Repository: ifBars/S1MAPI

Length of output: 4496


🏁 Script executed:

rg "InstantiateNetworked" --type cs -B 2 -A 2

Repository: ifBars/S1MAPI

Length of output: 3064


🏁 Script executed:

rg "NetworkObject" Core/PrefabRef.cs -B 3 -A 3

Repository: ifBars/S1MAPI

Length of output: 2534


🏁 Script executed:

rg "NetworkObject" --type cs -l | head -20

Repository: ifBars/S1MAPI

Length of output: 104


🏁 Script executed:

rg "InstantiateNetworked" --type cs | grep -i "test\|validate"

Repository: ifBars/S1MAPI

Length of output: 39


Add validation to fail fast if instantiated prefab lacks NetworkObject component.

At line 203, the method silently skips spawning if GetComponent<NetworkObject>() returns null, yet the documentation explicitly states that networked prefabs MUST have a NetworkObject component. This creates an undetected client/server desync risk when developers inadvertently call InstantiateNetworked() on non-networked prefabs.

🔧 Proposed fix
-            var netObj = instance.GetComponent<NetworkObject>();
-            if (netObj != null)
-            {
-                nm.ServerManager.Spawn(netObj);
-            }
+            var netObj = instance.GetComponent<NetworkObject>();
+            if (netObj == null)
+            {
+                DebugLog.Error($"[PrefabRef] Prefab '{Name}' is missing NetworkObject; cannot spawn networked.");
+                UnityEngine.Object.Destroy(instance);
+                return null;
+            }
+            nm.ServerManager.Spawn(netObj);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Core/PrefabRef.cs` around lines 203 - 220, The code currently silently skips
spawn when instance.GetComponent<NetworkObject>() returns null; update
InstantiateNetworked to fail fast: after var netObj =
instance.GetComponent<NetworkObject>(); if netObj is null then clean up the
created instance (Destroy(instance) or DestroyImmediate(instance) as
appropriate) and throw a clear InvalidOperationException (or ArgumentException)
stating that the prefab passed to InstantiateNetworked must contain a
NetworkObject to avoid client/server desync; reference InstantiateNetworked,
NetworkObject, and nm.ServerManager.Spawn in the message so callers can locate
the issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a known limitation. The current behavior means doors work for host and client (players and NPCs can open/close them), but door state doesn't sync between client/host (players appear to walk through doors). This should be fixed, but I don't think this PR has to fix it. My resolution is to accept it as a bug until a proper fix is made later. Leave unresolved for the maintainer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hdlmrell, understood — thanks for the context. The silent skip allowing local door functionality while deferring the sync fix is a reasonable short-term tradeoff. I'll leave this comment unresolved for the maintainer as requested.

Would you like me to open a GitHub issue to track the door state sync bug (players appearing to walk through doors due to missing NetworkObject spawn when GetComponent<NetworkObject>() returns null) so it doesn't get lost before a proper fix lands?


🧠 Learnings used
Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/BuildingUtilities.cs:158-170
Timestamp: 2026-03-28T16:03:16.357Z
Learning: In `Building/BuildingUtilities.cs` (`S1MAPI` project), `IsLivingEntity` currently uses `Animator` as the proxy for living entities (NPCs/players), which is acknowledged as too broad (animated props would match). The preferred fix is to check for `NavMeshAgent || CharacterController` instead — NPCs have `NavMeshAgent` (confirmed via `InteriorNavigatorCore` TrackedNPC.Agent), and players have `CharacterController`. An optional static `Func<Transform, bool>? LivingEntityOverride` delegate on `BuildingUtilities` can be added as an escape hatch for game-specific component checks without breaking consumers.

Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/InteriorNavigatorCore.cs:375-396
Timestamp: 2026-03-28T16:32:52.505Z
Learning: In `Building/InteriorNavigatorCore.cs` (`S1MAPI` project), `SetDestinationPrefix` intentionally adds a `TrackedNPC` to `_tracked`/`_globallyManaged` and returns `false` (blocks the original) even when the `NavMeshAgent` is null/disabled/off-navmesh. The null/disabled agent case is handled by a 12-second timeout in `UpdateApproaching`. Returning `true` (allowing the original) when the agent is unavailable would route the NPC into a carved-out NavMesh interior where it cannot navigate, which is a worse outcome. Do not suggest adding an early-return guard for unavailable agents in `SetDestinationPrefix`.

Learnt from: hdlmrell
Repo: ifBars/S1MAPI PR: 6
File: Building/Structural/TerrainClearer.cs:159-160
Timestamp: 2026-03-29T18:40:55.825Z
Learning: In `Building/Structural/TerrainClearer.cs` (`S1MAPI` project), `ClearAroundBuilding` uses `ComputeWorldBounds` which projects the building's actual corners into world space, producing a rotation-aware AABB. For rotated buildings the AABB is a conservative envelope that slightly over-clears the diagonal corners; this tradeoff is intentional and acceptable by design. Do not flag the bounds computation or the t.position containment test as footprint-inaccurate in future reviews. `ClearingOptions.Filter` and `ProtectedKeywords` are the intended escape hatches for edge cases.

Comment thread ProceduralMesh/Generators/Organic/OrganicShapeGenerator.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
Building/BuildingBuilder.cs (2)

150-172: ⚠️ Potential issue | 🟠 Major

Floor/ceiling palette overrides are still order-dependent.

GetDecorBuilder(...) memoizes a single builder, so the palette cloned here only matters if no earlier decor step has already created _decorBuilder. Any later AddFloor(...) / AddCeiling(...) override then renders with stale materials/colors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 150 - 172, The memoized single
decor builder returned by GetDecorBuilder causes palette overrides in
AddFloor/AddCeiling to be ignored if the builder was already created; update
GetDecorBuilder usage so palette-specific builders are created or the
memoization keyed by palette: either change GetDecorBuilder to accept a Palette
parameter and return/ cache distinct builders per palette, or when
color/material overrides are provided in AddFloor/AddCeiling, do not reuse the
existing _decorBuilder (reset/replace it) before calling AddFloor/AddCeiling;
reference GetDecorBuilder, _decorBuilder, _config.Palette, AddFloor, AddCeiling
and _registry.Register when making the change.

564-579: ⚠️ Potential issue | 🟠 Major

Navigation stair metadata still ignores the actual stair geometry.

_stairs only stores wall/foundation/width/offset here, but ComputeStairBasePosition() later reconstructs the run from default step height/depth and ignores maxStepHeight, stepDepth, style, flushWithFloor, and gap. Any non-default staircase can therefore produce a doorway link that does not line up with the built stairs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 564 - 579, The AddStairs method
currently appends only (wall, foundationHeight, width, lateralOffset) to the
_stairs collection but later ComputeStairBasePosition reconstructs stair
geometry from defaults; change the stored tuple in _stairs to include
maxStepHeight, stepDepth, style, flushWithFloor, and gap (and lateralOffset) so
the navigation metadata reflects the actual parameters passed to AddStairs, and
update ComputeStairBasePosition to read these stored values instead of using
Defaults/Spa­tial constants so doorway links align with the built stairs; keep
the existing call to GetDecorBuilder().AddStairs and _registry.Register
unchanged, just expand the saved data and consume it in
ComputeStairBasePosition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Building/BuildingBuilder.cs`:
- Around line 300-303: WithInteriorWallLayer currently updates
_interiorWallLayer but is ignored once _interiorWallBuilder is created (so
subsequent calls are a silent no-op); change WithInteriorWallLayer to either
reject late calls or apply the new layer to the cached builder: check
_interiorWallBuilder in WithInteriorWallLayer and if non-null either throw an
InvalidOperationException (reject late calls) or update/recreate
_interiorWallBuilder to reflect the new layer (e.g., set its layer property or
recreate it using the new _interiorWallLayer) so AddInteriorWall uses the
updated value; update unit tests to cover both behaviors if present.
- Around line 743-749: The AddSlidingDoors method currently calls
GetPrefabPlacer().PlaceSlidingDoors(position, rotation, openingHours,
Materials.MetalDarkGrey) and then invokes onCreated on the returned instance,
which runs server-side customization too late; instead, forward the onCreated
callback into PrefabPlacer.PlaceSlidingDoors so the placement path can call it
at the proper pre-activation point. Update BuildingBuilder.AddSlidingDoors to
pass onCreated into GetPrefabPlacer().PlaceSlidingDoors(...) (and remove the
local onCreated?.Invoke(instance) call), ensuring PrefabPlacer.PlaceSlidingDoors
signature/usage accepts and invokes the callback at the appropriate time.

---

Duplicate comments:
In `@Building/BuildingBuilder.cs`:
- Around line 150-172: The memoized single decor builder returned by
GetDecorBuilder causes palette overrides in AddFloor/AddCeiling to be ignored if
the builder was already created; update GetDecorBuilder usage so
palette-specific builders are created or the memoization keyed by palette:
either change GetDecorBuilder to accept a Palette parameter and return/ cache
distinct builders per palette, or when color/material overrides are provided in
AddFloor/AddCeiling, do not reuse the existing _decorBuilder (reset/replace it)
before calling AddFloor/AddCeiling; reference GetDecorBuilder, _decorBuilder,
_config.Palette, AddFloor, AddCeiling and _registry.Register when making the
change.
- Around line 564-579: The AddStairs method currently appends only (wall,
foundationHeight, width, lateralOffset) to the _stairs collection but later
ComputeStairBasePosition reconstructs stair geometry from defaults; change the
stored tuple in _stairs to include maxStepHeight, stepDepth, style,
flushWithFloor, and gap (and lateralOffset) so the navigation metadata reflects
the actual parameters passed to AddStairs, and update ComputeStairBasePosition
to read these stored values instead of using Defaults/Spa­tial constants so
doorway links align with the built stairs; keep the existing call to
GetDecorBuilder().AddStairs and _registry.Register unchanged, just expand the
saved data and consume it in ComputeStairBasePosition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ef21840-cbcd-4b5c-88a4-1d106f6d4d38

📥 Commits

Reviewing files that changed from the base of the PR and between c4b9eb3 and 9be6d4e.

📒 Files selected for processing (3)
  • Building/BuildingBuilder.cs
  • Building/BuildingPartRegistry.cs
  • Building/InteriorNavigatorCore.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • Building/BuildingPartRegistry.cs
  • Building/InteriorNavigatorCore.cs

Comment thread Building/BuildingBuilder.cs
Comment thread Building/BuildingBuilder.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Building/BuildingBuilder.cs (1)

88-93: ⚠️ Potential issue | 🟠 Major

Reconfiguration leaves stale build metadata behind.

WithConfig(), WithPalette(), and DefineRoom() all call InvalidateBuilders(), but this only nulls the helper builders. The new state introduced in this PR—wall openings, _foundationHeight, _stairs, registry entries, and already-emitted hierarchy state—survives, so reconfiguring a partially-built builder can make later AddDoorFrames(), FlattenTerrain(), CreateNavigationBuilder(), and Registry queries operate on stale metadata against the new room size. Either clear that state too, or reject reconfiguration after generation has started.

Also applies to: 101-105, 115-121, 821-828

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 88 - 93,
WithConfig/WithPalette/DefineRoom currently only null helper builders via
InvalidateBuilders(), leaving new state like wall openings, _foundationHeight,
_stairs, registry entries and emitted hierarchy state intact; update the code so
reconfiguration either fully resets that derived state or prevents
reconfiguration after generation has started. Specifically, modify
InvalidateBuilders() (or add a new InvalidateAllDerivedState() called from it)
to clear wall opening collections, reset _foundationHeight and _stairs to
defaults, clear any Registry entries and emitted-hierarchy flags/state used by
AddDoorFrames/FlattenTerrain/CreateNavigationBuilder, or alternately add a guard
at the start of WithConfig, WithPalette and DefineRoom to throw/return if a
generation-started flag is set; ensure you reference and update the symbols
InvalidateBuilders(), WithConfig(), WithPalette(), DefineRoom(),
_foundationHeight, _stairs, Registry, AddDoorFrames(), FlattenTerrain(),
CreateNavigationBuilder() when making the change.
♻️ Duplicate comments (4)
Building/BuildingBuilder.cs (4)

711-717: ⚠️ Potential issue | 🟠 Major

Forward onCreated into PrefabPlacer instead of invoking it after Place*() returns.

PrefabPlacer.PlaceInternal(...) in Building/Components/PrefabPlacer.cs:173-229 already has the right pre-activation/deferred-link hook. Calling onCreated only after Place(...)/PlaceSlidingDoors(...) returns means server-side customization runs too late, and queued networked prefabs never invoke the callback when the client-side linker resolves. Pass the callback through to the placer and remove the local invocation here.

Also applies to: 753-759

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 711 - 717, The AddPrefab method in
BuildingBuilder currently invokes the onCreated callback after calling
GetPrefabPlacer().Place(...), which runs too late for server-side customization
and misses deferred networked prefab linking; instead forward the onCreated
Action<GameObject>? into the PrefabPlacer.Place (and PlaceSlidingDoors) call so
the placer can invoke it pre-activation via its PlaceInternal hook, and remove
the local onCreated?.Invoke(instance) call; apply the same change to the other
AddPrefab overload (the one referenced around lines 753-759) so both call sites
pass the callback through to PrefabPlacer.

135-150: ⚠️ Potential issue | 🟠 Major

Per-call floor/ceiling overrides are still order-dependent.

These overrides are only threaded through GetDecorBuilder(palette), but GetDecorBuilder() memoizes a single DecorBuilder. Once any earlier decor call has initialized _decorBuilder, later AddFloor(...)/AddCeiling(...) overrides are ignored and the old palette is reused. Pass the overrides into DecorBuilder.AddFloor/AddCeiling directly, or reject per-call overrides after _decorBuilder exists.

Also applies to: 161-172

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 135 - 150, The per-call
floor/ceiling overrides in BuildingBuilder.AddFloor are lost when
GetDecorBuilder() returns an already-memoized _decorBuilder; to fix, thread the
computed palette overrides into the DecorBuilder call instead of only into
GetDecorBuilder: call GetDecorBuilder() to obtain the builder but pass the local
palette (or null check) into DecorBuilder.AddFloor and DecorBuilder.AddCeiling
so each call uses the provided palette, or alternatively throw/reject when a
caller provides per-call color/material overrides after _decorBuilder has
already been created; update BuildingBuilder.AddFloor (and the analogous
AddCeiling code) to either forward the palette into
DecorBuilder.AddFloor/AddCeiling or to validate and reject overrides if
_decorBuilder != null.

300-303: ⚠️ Potential issue | 🟠 Major

WithInteriorWallLayer() becomes a silent no-op after the first interior wall.

GetInteriorWallBuilder() captures _interiorWallLayer only on first use. After AddInteriorWall() has created _interiorWallBuilder, changing the layer here no longer affects subsequent walls, so the API silently stops honoring the caller’s input. Either throw once interior walls exist, or recreate/update the cached builder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 300 - 303, WithInteriorWallLayer
currently only sets _interiorWallLayer but becomes a silent no-op if an
_interiorWallBuilder was already created (GetInteriorWallBuilder captured the
layer), so either prevent confusing behavior or keep the cached builder in sync:
update WithInteriorWallLayer to check whether _interiorWallBuilder is non-null
and either throw an InvalidOperationException (prevent changing layer after
AddInteriorWall/GetInteriorWallBuilder) or recreate/update _interiorWallBuilder
to reflect the new _interiorWallLayer; reference the methods
WithInteriorWallLayer, GetInteriorWallBuilder, AddInteriorWall and fields
_interiorWallBuilder and _interiorWallLayer when implementing the chosen
approach.

574-589: ⚠️ Potential issue | 🟠 Major

Persist the real stair spec instead of a partial snapshot.

_stairs only stores (wall, foundationHeight, width, offset), and that offset is captured before AddWalls() may have populated a non-zero door offset. ComputeStairBasePosition() then rebuilds the run from defaults, ignoring maxStepHeight, stepDepth, style, flushWithFloor, and gap, so nav approach points drift from the actual staircase as soon as the door is offset or the stair settings are customized. Store a full stair spec (or the created stair transform) and derive the base point from that.

Also applies to: 971-990

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 574 - 589, The _stairs list
currently stores a partial tuple (wall, foundationHeight, width, offset)
captured before door offsets and ignores the actual stair parameters used by
GetDecorBuilder().AddStairs, causing ComputeStairBasePosition() to rebuild
incorrect runs; update AddStairs to persist the full stair specification
(include maxStepHeight, stepDepth, color/material/style, flushWithFloor, gap and
the computed transform or the returned stairs object) instead of the partial
tuple, register that full spec/transform with
_registry.Register(BuildingPart.Stairs, stairs), and update
ComputeStairBasePosition() to derive the base point from the stored stair
spec/transform; apply the same fix for the duplicate code path that mirrors this
logic.
🧹 Nitpick comments (1)
Utils/Constants.cs (1)

429-441: Consider defensive copies for readonly arrays.

DefaultVegetationKeywords and DefaultProtectedKeywords are static readonly, meaning the reference is immutable but the array contents can be modified at runtime. If any caller inadvertently modifies these arrays, it affects all consumers.

Options to consider:

  1. Return copies via properties: public static string[] DefaultVegetationKeywords => new[] { ... };
  2. Use IReadOnlyList<string> backed by a private array
  3. Accept the current approach if mutation risk is low in practice
🛡️ Example using IReadOnlyList for immutability
-            public static readonly string[] DefaultVegetationKeywords =
-            {
-                "Rock", "Boulder", "Shrub", "Bush", "Tree rustle", "Foliage"
-            };
+            private static readonly string[] _defaultVegetationKeywords =
+            {
+                "Rock", "Boulder", "Shrub", "Bush", "Tree rustle", "Foliage"
+            };
+            public static IReadOnlyList<string> DefaultVegetationKeywords => _defaultVegetationKeywords;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Utils/Constants.cs` around lines 429 - 441, The two public static readonly
arrays DefaultVegetationKeywords and DefaultProtectedKeywords are mutable even
though the field references are readonly; change their public exposure to an
immutable contract by replacing the array fields with either public static
properties that return new string[] copies or by exposing them as public static
IReadOnlyList<string> backed by private readonly arrays (e.g., keep a private
readonly string[] _defaultVegetationKeywords and expose public static
IReadOnlyList<string> DefaultVegetationKeywords => _defaultVegetationKeywords),
ensuring callers cannot modify the shared contents; update any usage sites to
the new property type if needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Building/BuildingBuilder.cs`:
- Around line 88-93: WithConfig/WithPalette/DefineRoom currently only null
helper builders via InvalidateBuilders(), leaving new state like wall openings,
_foundationHeight, _stairs, registry entries and emitted hierarchy state intact;
update the code so reconfiguration either fully resets that derived state or
prevents reconfiguration after generation has started. Specifically, modify
InvalidateBuilders() (or add a new InvalidateAllDerivedState() called from it)
to clear wall opening collections, reset _foundationHeight and _stairs to
defaults, clear any Registry entries and emitted-hierarchy flags/state used by
AddDoorFrames/FlattenTerrain/CreateNavigationBuilder, or alternately add a guard
at the start of WithConfig, WithPalette and DefineRoom to throw/return if a
generation-started flag is set; ensure you reference and update the symbols
InvalidateBuilders(), WithConfig(), WithPalette(), DefineRoom(),
_foundationHeight, _stairs, Registry, AddDoorFrames(), FlattenTerrain(),
CreateNavigationBuilder() when making the change.

---

Duplicate comments:
In `@Building/BuildingBuilder.cs`:
- Around line 711-717: The AddPrefab method in BuildingBuilder currently invokes
the onCreated callback after calling GetPrefabPlacer().Place(...), which runs
too late for server-side customization and misses deferred networked prefab
linking; instead forward the onCreated Action<GameObject>? into the
PrefabPlacer.Place (and PlaceSlidingDoors) call so the placer can invoke it
pre-activation via its PlaceInternal hook, and remove the local
onCreated?.Invoke(instance) call; apply the same change to the other AddPrefab
overload (the one referenced around lines 753-759) so both call sites pass the
callback through to PrefabPlacer.
- Around line 135-150: The per-call floor/ceiling overrides in
BuildingBuilder.AddFloor are lost when GetDecorBuilder() returns an
already-memoized _decorBuilder; to fix, thread the computed palette overrides
into the DecorBuilder call instead of only into GetDecorBuilder: call
GetDecorBuilder() to obtain the builder but pass the local palette (or null
check) into DecorBuilder.AddFloor and DecorBuilder.AddCeiling so each call uses
the provided palette, or alternatively throw/reject when a caller provides
per-call color/material overrides after _decorBuilder has already been created;
update BuildingBuilder.AddFloor (and the analogous AddCeiling code) to either
forward the palette into DecorBuilder.AddFloor/AddCeiling or to validate and
reject overrides if _decorBuilder != null.
- Around line 300-303: WithInteriorWallLayer currently only sets
_interiorWallLayer but becomes a silent no-op if an _interiorWallBuilder was
already created (GetInteriorWallBuilder captured the layer), so either prevent
confusing behavior or keep the cached builder in sync: update
WithInteriorWallLayer to check whether _interiorWallBuilder is non-null and
either throw an InvalidOperationException (prevent changing layer after
AddInteriorWall/GetInteriorWallBuilder) or recreate/update _interiorWallBuilder
to reflect the new _interiorWallLayer; reference the methods
WithInteriorWallLayer, GetInteriorWallBuilder, AddInteriorWall and fields
_interiorWallBuilder and _interiorWallLayer when implementing the chosen
approach.
- Around line 574-589: The _stairs list currently stores a partial tuple (wall,
foundationHeight, width, offset) captured before door offsets and ignores the
actual stair parameters used by GetDecorBuilder().AddStairs, causing
ComputeStairBasePosition() to rebuild incorrect runs; update AddStairs to
persist the full stair specification (include maxStepHeight, stepDepth,
color/material/style, flushWithFloor, gap and the computed transform or the
returned stairs object) instead of the partial tuple, register that full
spec/transform with _registry.Register(BuildingPart.Stairs, stairs), and update
ComputeStairBasePosition() to derive the base point from the stored stair
spec/transform; apply the same fix for the duplicate code path that mirrors this
logic.

---

Nitpick comments:
In `@Utils/Constants.cs`:
- Around line 429-441: The two public static readonly arrays
DefaultVegetationKeywords and DefaultProtectedKeywords are mutable even though
the field references are readonly; change their public exposure to an immutable
contract by replacing the array fields with either public static properties that
return new string[] copies or by exposing them as public static
IReadOnlyList<string> backed by private readonly arrays (e.g., keep a private
readonly string[] _defaultVegetationKeywords and expose public static
IReadOnlyList<string> DefaultVegetationKeywords => _defaultVegetationKeywords),
ensuring callers cannot modify the shared contents; update any usage sites to
the new property type if needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ebb971b7-abfe-4b37-afb9-234ff35d7612

📥 Commits

Reviewing files that changed from the base of the PR and between 9be6d4e and 2cef68b.

📒 Files selected for processing (3)
  • Building/BuildingBuilder.cs
  • Building/InteriorNavigatorCore.cs
  • Utils/Constants.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • Building/InteriorNavigatorCore.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Building/BuildingBuilder.cs (1)

135-156: ⚠️ Potential issue | 🟠 Major

Don't turn one-off floor/ceiling overrides into shared builder state.

These assignments mutate _config.Palette itself, so a single AddFloor(color: ...) / AddCeiling(material: ...) call changes the defaults seen by later steps and can also mutate a palette instance that was passed in through WithPalette(...). That makes an optional per-call override sticky. Prefer threading explicit overrides into DecorBuilder.AddFloor/AddCeiling instead of rewriting the shared config object.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 135 - 156, BuildingBuilder.AddFloor
and AddCeiling currently mutate _config.Palette (setting FloorColor/CeilingColor
and FloorMaterial/CeilingMaterial) which makes one-off overrides sticky and can
mutate a palette supplied via WithPalette; instead, do not change
_config.Palette—capture the optional color/material into local variables and
pass them as explicit overrides into GetDecorBuilder().AddFloor(...) and
GetDecorBuilder().AddCeiling(...), or add overloads to those DecorBuilder
methods to accept the per-call color/material, so the shared _config.Palette
remains unchanged.
♻️ Duplicate comments (4)
Building/BuildingBuilder.cs (4)

738-744: ⚠️ Potential issue | 🟠 Major

AddSlidingDoors() still invokes onCreated too late.

This callback needs to flow through PrefabPlacer.PlaceSlidingDoors(...) so it runs before activation and on the deferred client-link path. The local post-return invoke only customizes the server instance after it is already live.

Suggested change
-            GameObject? instance = GetPrefabPlacer().PlaceSlidingDoors(position, rotation, openingHours, Materials.MetalDarkGrey);
-            if (instance != null)
-            {
-                onCreated?.Invoke(instance);
-            }
+            GetPrefabPlacer().PlaceSlidingDoors(
+                position, rotation, openingHours, Materials.MetalDarkGrey, onCreated);
             return this;

PrefabPlacer.PlaceSlidingDoors(...) will need the matching callback overload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 738 - 744, AddSlidingDoors
currently invokes onCreated after PlaceSlidingDoors returns, which is too late;
change PrefabPlacer.PlaceSlidingDoors to accept the same Action<GameObject>?
callback and have PlaceSlidingDoors invoke it before activating the prefab and
on the deferred client-link path, then update BuildingBuilder.AddSlidingDoors to
pass through the onCreated callback instead of invoking it locally (remove the
local post-return onCreated?.Invoke call) and adjust any other callers to the
new PlaceSlidingDoors overload.

285-288: ⚠️ Potential issue | 🟠 Major

WithInteriorWallLayer() still becomes a silent no-op after the first interior wall.

_interiorWallLayer is only read when GetInteriorWallBuilder() allocates the cached builder. After any AddInteriorWall(), changing the layer here no longer affects subsequent walls. Either reject late calls or rebuild/apply the new layer to the cached builder.

Suggested change
 public BuildingBuilder WithInteriorWallLayer(int layer)
 {
+    if (_interiorWallBuilder != null)
+        throw new System.InvalidOperationException(
+            "[BuildingBuilder.WithInteriorWallLayer] Call this before AddInteriorWall().");
     _interiorWallLayer = layer;
     return this;
 }

Also applies to: 847-850

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 285 - 288, WithInteriorWallLayer
currently only updates _interiorWallLayer and becomes a silent no-op after the
cached interior wall builder is created in GetInteriorWallBuilder, so change
WithInteriorWallLayer to either validate timing or apply the new value to the
cached builder: detect if the cached InteriorWallBuilder instance exists (the
field created by GetInteriorWallBuilder), and if so update its layer
property/state to the new value; otherwise just set _interiorWallLayer as
before. Also ensure behavior is mirrored for the duplicate case referenced
(lines 847-850) and consider throwing an ArgumentException from
WithInteriorWallLayer if you prefer to reject changes after AddInteriorWall has
been called.

559-573: ⚠️ Potential issue | 🟠 Major

Stair navigation still doesn't match the generated stair geometry.

AddStairs() builds from the full stair spec, but the builder only keeps a partial tuple and ComputeStairBasePosition() rebuilds the run from default constants; even the stored width/offset are ignored here. That still drops stepDepth, maxStepHeight, gap, flushWithFloor, and style, so custom stairs or later doorway-offset changes can put the approach point beside the real staircase. The hard-coded foundationHeight = 2.0f default also desynchronizes immediately from any custom AddFoundation(height: ...). Persist the actual stair run/spec and default or validate against _foundationHeight.

Also applies to: 956-976

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 559 - 573, AddStairs currently
stores only a partial tuple in _stairs (wall, foundationHeight, width,
lateralOffset) while the actual stair spec (maxStepHeight, stepDepth, gap,
flushWithFloor, style) is used to build geometry, causing
ComputeStairBasePosition to reconstruct a run from defaults and mismatch
navigation; fix by persisting the full stair spec when AddStairs calls
GetDecorBuilder().AddStairs (store a struct/class or full tuple including
maxStepHeight, stepDepth, gap, flushWithFloor, style and the lateralOffset) and
update ComputeStairBasePosition to read the stored spec (and validate or clamp
foundationHeight against _foundationHeight) instead of using hard-coded
constants so navigation aligns with the generated stair geometry.

696-702: ⚠️ Potential issue | 🟠 Major

Pass onCreated through PrefabPlacer.Place(...) instead of invoking it afterward.

PrefabPlacer already has the right pre-activation/deferred-link hook. Calling the callback only after Place() returns runs too late for Awake/OnEnable-sensitive setup on the server and skips the deferred client-link path for networked prefabs.

Suggested change
-            GameObject? instance = GetPrefabPlacer().Place(prefab, position, rotation);
-            if (instance != null)
-            {
-                onCreated?.Invoke(instance);
-            }
+            GetPrefabPlacer().Place(prefab, position, rotation, onCreated);
             return this;

PrefabPlacer.Place(...) will need the matching callback overload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 696 - 702, The AddPrefab method
currently calls GetPrefabPlacer().Place(prefab, position, rotation) and invokes
onCreated after Place returns, which is too late for Awake/OnEnable and deferred
client-link hooks; change AddPrefab (BuildingBuilder.AddPrefab) to pass the
onCreated callback into PrefabPlacer.Place (e.g., PrefabPlacer.Place(prefab,
position, rotation, onCreated)) and update PrefabPlacer.Place to provide an
overload that accepts and invokes that callback at the proper
pre-activation/deferred-link point inside Place so the callback runs during
placement instead of after placement returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Building/BuildingBuilder.cs`:
- Around line 388-394: FlattenTerrain currently calls
TerrainFlattener.FlattenUnder with the raw _roomSize, ignoring any expansion
applied by AddFoundation (expandX/expandZ), which leaves terrain under overhangs
unflattened; update FlattenTerrain (and the similar call at lines ~534-537) to
compute the actual footprint by expanding _roomSize by the foundation expansion
values (expandX, expandZ) used in AddFoundation and pass that expanded size to
TerrainFlattener.FlattenUnder, ensuring you still use _root and
_foundationHeight for targetWorldY and keep padding, clearDetails, blendDistance
unchanged.
- Around line 336-337: InteriorDoorways currently returns
_interiorWallBuilder?.Doorways which is lost when InvalidateBuilders() nulls
_interiorWallBuilder; update the class to persist doorway metadata outside the
lazy builder by caching the doorways (e.g. a private IReadOnlyList<DoorwayInfo>
_interiorDoorwaysCache) and change InteriorDoorways to return the cache fallback
when _interiorWallBuilder is null, and in InvalidateBuilders() copy
_interiorWallBuilder.Doorways into the cache before clearing the builder
(alternatively, reconstruct the interior wall builder and repopulate its
Doorways before nulling it). Ensure CreateNavigationBuilder() and
AddInteriorDoorFrames() read from InteriorDoorways so door openings survive
subsequent WithConfig/WithPalette/DefineRoom calls.

---

Outside diff comments:
In `@Building/BuildingBuilder.cs`:
- Around line 135-156: BuildingBuilder.AddFloor and AddCeiling currently mutate
_config.Palette (setting FloorColor/CeilingColor and
FloorMaterial/CeilingMaterial) which makes one-off overrides sticky and can
mutate a palette supplied via WithPalette; instead, do not change
_config.Palette—capture the optional color/material into local variables and
pass them as explicit overrides into GetDecorBuilder().AddFloor(...) and
GetDecorBuilder().AddCeiling(...), or add overloads to those DecorBuilder
methods to accept the per-call color/material, so the shared _config.Palette
remains unchanged.

---

Duplicate comments:
In `@Building/BuildingBuilder.cs`:
- Around line 738-744: AddSlidingDoors currently invokes onCreated after
PlaceSlidingDoors returns, which is too late; change
PrefabPlacer.PlaceSlidingDoors to accept the same Action<GameObject>? callback
and have PlaceSlidingDoors invoke it before activating the prefab and on the
deferred client-link path, then update BuildingBuilder.AddSlidingDoors to pass
through the onCreated callback instead of invoking it locally (remove the local
post-return onCreated?.Invoke call) and adjust any other callers to the new
PlaceSlidingDoors overload.
- Around line 285-288: WithInteriorWallLayer currently only updates
_interiorWallLayer and becomes a silent no-op after the cached interior wall
builder is created in GetInteriorWallBuilder, so change WithInteriorWallLayer to
either validate timing or apply the new value to the cached builder: detect if
the cached InteriorWallBuilder instance exists (the field created by
GetInteriorWallBuilder), and if so update its layer property/state to the new
value; otherwise just set _interiorWallLayer as before. Also ensure behavior is
mirrored for the duplicate case referenced (lines 847-850) and consider throwing
an ArgumentException from WithInteriorWallLayer if you prefer to reject changes
after AddInteriorWall has been called.
- Around line 559-573: AddStairs currently stores only a partial tuple in
_stairs (wall, foundationHeight, width, lateralOffset) while the actual stair
spec (maxStepHeight, stepDepth, gap, flushWithFloor, style) is used to build
geometry, causing ComputeStairBasePosition to reconstruct a run from defaults
and mismatch navigation; fix by persisting the full stair spec when AddStairs
calls GetDecorBuilder().AddStairs (store a struct/class or full tuple including
maxStepHeight, stepDepth, gap, flushWithFloor, style and the lateralOffset) and
update ComputeStairBasePosition to read the stored spec (and validate or clamp
foundationHeight against _foundationHeight) instead of using hard-coded
constants so navigation aligns with the generated stair geometry.
- Around line 696-702: The AddPrefab method currently calls
GetPrefabPlacer().Place(prefab, position, rotation) and invokes onCreated after
Place returns, which is too late for Awake/OnEnable and deferred client-link
hooks; change AddPrefab (BuildingBuilder.AddPrefab) to pass the onCreated
callback into PrefabPlacer.Place (e.g., PrefabPlacer.Place(prefab, position,
rotation, onCreated)) and update PrefabPlacer.Place to provide an overload that
accepts and invokes that callback at the proper pre-activation/deferred-link
point inside Place so the callback runs during placement instead of after
placement returns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e199fa33-f60d-4501-8cad-35f1146e5ef0

📥 Commits

Reviewing files that changed from the base of the PR and between 2cef68b and 32ac0be.

📒 Files selected for processing (1)
  • Building/BuildingBuilder.cs

Comment thread Building/BuildingBuilder.cs Outdated
Comment thread Building/BuildingBuilder.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
Building/BuildingPartRegistry.cs (1)

51-63: Consider pre-sizing the renderers list to reduce allocations.

For large buildings with many parts, GetComponentsInChildren<Renderer>() can return substantial arrays. The current implementation adds them one-by-one via AddRange. A minor optimization would be to estimate capacity, though this is not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingPartRegistry.cs` around lines 51 - 63, GetRenderers
currently builds renderers by repeatedly calling AddRange which causes multiple
resizes; fix by first estimating total capacity then allocating the List once.
Inside GetRenderers (use the existing _parts.TryGetValue and local list
variable), do a first pass over list to sum the lengths of
list[i].GetComponentsInChildren<Renderer>() (skip nulls), create renderers = new
List<Renderer>(totalCount), then do a second pass to AddRange each non-null
array and finally return renderers.ToArray(); this reduces intermediate
allocations.
Building/BuildingBuilder.cs (1)

166-174: Direct palette mutation may have unintended side effects.

Lines 168-169 directly mutate _config.Palette.FloorColor and _config.Palette.FloorMaterial. If the palette is shared (e.g., from a preset or passed via WithPalette), this modifies the original instance rather than creating an override.

Consider cloning the palette before mutation (similar to AddWalls at lines 223-228) or documenting that overrides permanently modify the config.

Proposed fix
 public BuildingBuilder AddFloor(Color? color = null, Material? material = null)
 {
-    if (color.HasValue) _config.Palette.FloorColor = color.Value;
-    if (material != null) _config.Palette.FloorMaterial = material;
+    if (color.HasValue || material != null)
+    {
+        _config.Palette = _config.Palette.Clone();
+        if (color.HasValue) _config.Palette.FloorColor = color.Value;
+        if (material != null) _config.Palette.FloorMaterial = material;
+    }
 
     var floor = GetDecorBuilder().AddFloor(_config.FloorThickness);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Building/BuildingBuilder.cs` around lines 166 - 174, AddFloor currently
mutates _config.Palette directly (setting _config.Palette.FloorColor and
FloorMaterial), which can unintentionally change a shared palette; update
AddFloor to ensure the palette is cloned before applying overrides (follow the
pattern used in AddWalls: create a new Palette instance or call the existing
clone/Copy method on _config.Palette, assign it back to _config.Palette, then
set FloorColor/FloorMaterial), so overrides only affect this builder's config
and not any shared palette instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Building/BuildingBuilder.cs`:
- Around line 166-174: AddFloor currently mutates _config.Palette directly
(setting _config.Palette.FloorColor and FloorMaterial), which can
unintentionally change a shared palette; update AddFloor to ensure the palette
is cloned before applying overrides (follow the pattern used in AddWalls: create
a new Palette instance or call the existing clone/Copy method on
_config.Palette, assign it back to _config.Palette, then set
FloorColor/FloorMaterial), so overrides only affect this builder's config and
not any shared palette instances.

In `@Building/BuildingPartRegistry.cs`:
- Around line 51-63: GetRenderers currently builds renderers by repeatedly
calling AddRange which causes multiple resizes; fix by first estimating total
capacity then allocating the List once. Inside GetRenderers (use the existing
_parts.TryGetValue and local list variable), do a first pass over list to sum
the lengths of list[i].GetComponentsInChildren<Renderer>() (skip nulls), create
renderers = new List<Renderer>(totalCount), then do a second pass to AddRange
each non-null array and finally return renderers.ToArray(); this reduces
intermediate allocations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 046f3262-b11f-4b9e-81e3-911c7dc33a74

📥 Commits

Reviewing files that changed from the base of the PR and between 32ac0be and 7bcd984.

📒 Files selected for processing (5)
  • Building/BuildingBuilder.cs
  • Building/BuildingPartRegistry.cs
  • Building/BuildingUtilities.cs
  • Building/Structural/DecorBuilder.cs
  • Utils/Constants.cs

@ifBars
ifBars merged commit 4121b98 into ifBars:stable Mar 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants