Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
fa5bf4d
fix(Building): correct door top segment positioning above roofline
hdlmrell Feb 21, 2026
524cfa1
feat(Building): add TerrainClearer for clearing terrain around placed…
hdlmrell Feb 22, 2026
f9ee2a1
Merge branch 'fix/door-top-segment-positioning' into fix/urp-light-de…
hdlmrell Feb 22, 2026
0d53891
fix(Lighting): set URP-correct light defaults for Schedule I's pipeline
hdlmrell Feb 22, 2026
379d045
fix(Building): clear renderer-less objects and tree rustle sounds in …
hdlmrell Feb 22, 2026
2529131
feat(Building): add onCreated callback to AddPrefab and AddSlidingDoors
hdlmrell Feb 22, 2026
5955683
fix(Building): use instance IDs for preserved set and protect road we…
hdlmrell Feb 22, 2026
64a4cac
feat(Building): add stair style system with Solid, ClosedRiser, and O…
hdlmrell Feb 22, 2026
1eeb58a
fix(Building): gap base molding around door openings
hdlmrell Feb 22, 2026
f86e2c9
fix(Building): position base molding to protrude past walls like roof…
hdlmrell Feb 22, 2026
c7fff2a
feat(Building): add opt-in door frames with wall inset for clean fit
hdlmrell Feb 22, 2026
23ca7a6
feat(Building): add support for windows alongside door openings
hdlmrell Feb 23, 2026
fc6ef6b
feat(Building): make AddWalls boolean overload support doors and wind…
hdlmrell Feb 23, 2026
9eabccf
feat(Building): support multiple windows per wall and fix glass material
hdlmrell Feb 23, 2026
dbac6e5
feat(Building): add RoofBuilder with parapet and hip roof styles
hdlmrell Feb 23, 2026
f47b0e1
fix(Building): extend N/S walls to cover corner gaps with E/W walls
hdlmrell Feb 23, 2026
22d763f
feat(Building): add AddCornerTrim for vertical corner trim strips
hdlmrell Feb 23, 2026
5b83958
feat(Building): add WallOpening.Offset for off-center doors and windows
hdlmrell Feb 23, 2026
75b1297
feat(Building): add interior walls with doorway tracking for NavMesh
hdlmrell Feb 23, 2026
40da7b1
feat(Building): add WithInteriorWallLayer for placement raycast control
hdlmrell Feb 25, 2026
be633a8
feat(Building): add color and material parameters to AddFoundation an…
hdlmrell Feb 25, 2026
e0b262c
feat(Building): add NavMeshRepairer for runtime interior NavMesh
hdlmrell Feb 26, 2026
227f0e1
fix(Building): fix NavMesh doorway filter and add interior doorway su…
hdlmrell Mar 1, 2026
37e74c3
fix: replace foreach Transform with GetChild for IL2CPP compatibility
hdlmrell Mar 9, 2026
71d20c1
feat(Building): add per-wall material and color overrides for exterio…
hdlmrell Mar 10, 2026
5451ea9
feat(Building): add skipWalls parameter to AddBaseMolding
hdlmrell Mar 12, 2026
4ce9cda
feat(Building): add TerrainFlattener for runtime height and detail cl…
hdlmrell Mar 13, 2026
a2030f2
feat(Building): add interior door frames with wall segment insetting
hdlmrell Mar 15, 2026
0ab0bb7
fix(ProceduralMesh): enable all rendering layers for decal projection…
hdlmrell Mar 22, 2026
5bf084b
refactor(Building): replace SnapToGrid with ComputeGridCellSize and a…
hdlmrell Mar 22, 2026
fe5f29e
fix(Building): harden TerrainClearer with living entity detection and…
hdlmrell Mar 22, 2026
b1a032c
refactor(Building): remove dead heightmap snapshot system from Terrai…
hdlmrell Mar 22, 2026
85c9898
feat(Building): unify NPC navigation on custom A* pathfinding
hdlmrell Mar 22, 2026
6f2b83b
fix(Building): clear FoliageRustleSound objects during terrain clearing
hdlmrell Mar 23, 2026
96f54fa
fix(Building): improve NPC chase repath and stuck detection in interi…
hdlmrell Mar 24, 2026
957804b
fix(Building): add terrain retry queue for IL2CPP late-load race cond…
hdlmrell Mar 24, 2026
c705123
feat(Building): add client-side FishNet prefab linking and inactive s…
hdlmrell Mar 24, 2026
31de7a7
feat(Building): add BuildingPartRegistry and document multiplayer doo…
hdlmrell Mar 24, 2026
eddf87a
docs: fix XML doc generic syntax in OrganicShapeGenerator example
hdlmrell Mar 24, 2026
a19bdd0
docs: add advanced building example with terrain, navigation, and net…
hdlmrell Mar 24, 2026
010ef8d
docs: expand building guide with interiors, roofs, terrain, navigatio…
hdlmrell Mar 24, 2026
061f515
docs: add missing param tags to BuildingPartRegistry and NavigationBu…
hdlmrell Mar 24, 2026
04f7427
docs: update api-overview with new building systems and components
hdlmrell Mar 24, 2026
4502bfd
feat(Building): add interior/exterior dual-material support for exter…
hdlmrell Mar 25, 2026
c4b9eb3
feat(Building): stair doorway lerp, directed NPC routing, and interio…
hdlmrell Mar 27, 2026
a852e7f
fix(Building): replace MonoBehaviour with plain class for BuildingPar…
hdlmrell Mar 27, 2026
9be6d4e
fix(Building): prevent NavMeshAgent re-enable from warping NPCs out o…
hdlmrell Mar 28, 2026
51cebab
fix(Building): remove Vector3.zero guard from agent warp-back restore
hdlmrell Mar 28, 2026
b3b1298
docs: add missing XML summaries for Constants.Layers, Tags, and Roof …
hdlmrell Mar 28, 2026
2cef68b
fix(Building): fail fast in CreateNavigationBuilder when no exterior …
hdlmrell Mar 28, 2026
32ac0be
fix(Building): apply floor/ceiling overrides to palette before memoiz…
hdlmrell Mar 28, 2026
ea92629
fix(Building): use real stair params in ComputeStairBasePosition nav …
hdlmrell Mar 28, 2026
9be0a3f
fix(Building): guard against negative submeshIndex in SetSubmeshMaterial
hdlmrell Mar 28, 2026
7bcd984
refactor(Building): use NavMeshAgent/CharacterController for living e…
hdlmrell Mar 28, 2026
980e71f
fix(Building): restore HasDestination flag in ReleaseAllNPCs during c…
hdlmrell Mar 28, 2026
d9647ef
fix(Building): use ReferenceEquals for chase target null check to det…
hdlmrell Mar 29, 2026
90ee628
feat(Building): expose walkability queries on NavigationBuilder
hdlmrell Mar 29, 2026
f988e87
fix(Building): clamp door/window offsets to wall bounds with warning
hdlmrell Mar 29, 2026
2da788b
fix(Building): trigger dual-material walls on interior color-only ove…
hdlmrell Mar 29, 2026
acfe35d
docs(ProceduralMesh): fix generic syntax in OrganicShapeGenerator XML…
hdlmrell Mar 29, 2026
111c39f
fix(Building): reject WithInteriorWallLayer after AddInteriorWall
hdlmrell Mar 29, 2026
58fa051
fix(Building): move AddSlidingDoors onCreated callback to pre-activat…
hdlmrell Mar 29, 2026
3207abf
fix(Building): return false for out-of-bounds positions in IsWalkable
hdlmrell Mar 29, 2026
1d3dcd7
fix(Building): include foundation expansion in FlattenTerrain footprint
hdlmrell Mar 29, 2026
11b1aa2
fix(Building): preserve interior doorway metadata across InvalidateBu…
hdlmrell Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
678 changes: 632 additions & 46 deletions Building/BuildingBuilder.cs

Large diffs are not rendered by default.

215 changes: 215 additions & 0 deletions Building/BuildingPartRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
using System.Collections.Generic;
using S1MAPI.Building.Config;
using S1MAPI.Building.Structural;
using UnityEngine;

namespace S1MAPI.Building
{
/// <summary>
/// Post-build registry that tracks building parts by category.
/// Attached to the building root during construction by <see cref="BuildingBuilder"/>.
/// Consumers use this to target specific parts (all exterior walls, just the north wall,
/// the floor, trim, etc.) and get their renderers for material swaps.
/// </summary>
public sealed class BuildingPartRegistry
{
private readonly Dictionary<BuildingPart, List<GameObject>> _parts = new Dictionary<BuildingPart, List<GameObject>>();
private readonly Dictionary<WallSide, List<GameObject>> _wallsByDirection = new Dictionary<WallSide, List<GameObject>>();

#region Registration (called by BuildingBuilder during construction)

internal void Register(BuildingPart part, GameObject go)
{
if (!_parts.TryGetValue(part, out var list))
{
list = new List<GameObject>();
_parts[part] = list;
}
list.Add(go);
}

internal void Register(WallSide side, GameObject go)
{
if (!_wallsByDirection.TryGetValue(side, out var list))
{
list = new List<GameObject>();
_wallsByDirection[side] = list;
}
list.Add(go);

Register(BuildingPart.ExteriorWalls, go);
}

#endregion

#region Consumer API — Renderers

/// <summary>
/// Get all renderers for a building part category.
/// </summary>
/// <param name="part">The building part category to query.</param>
public Renderer[] GetRenderers(BuildingPart part)
{
if (!_parts.TryGetValue(part, out var list))
return System.Array.Empty<Renderer>();

var renderers = new List<Renderer>();
for (int i = 0; i < list.Count; i++)
{
if (list[i] != null)
renderers.AddRange(list[i].GetComponentsInChildren<Renderer>());
}
return renderers.ToArray();
}

/// <summary>
/// Get all renderers for a specific wall direction.
/// </summary>
/// <param name="side">The specific wall side to query.</param>
public Renderer[] GetRenderers(WallSide side)
{
if (!_wallsByDirection.TryGetValue(side, out var list))
return System.Array.Empty<Renderer>();

var renderers = new List<Renderer>();
for (int i = 0; i < list.Count; i++)
{
if (list[i] != null)
renderers.AddRange(list[i].GetComponentsInChildren<Renderer>());
}
return renderers.ToArray();
}

#endregion

#region Consumer API — GameObjects

/// <summary>
/// Get all GameObjects for a building part category.
/// </summary>
/// <param name="part">The building part category to query.</param>
public GameObject[] GetParts(BuildingPart part)
{
if (!_parts.TryGetValue(part, out var list))
return System.Array.Empty<GameObject>();

var result = new List<GameObject>(list.Count);
for (int i = 0; i < list.Count; i++)
{
if (list[i] != null)
result.Add(list[i]);
}
return result.ToArray();
}

/// <summary>
/// Get all GameObjects for a specific wall direction.
/// </summary>
/// <param name="side">The specific wall side to query.</param>
public GameObject[] GetParts(WallSide side)
{
if (!_wallsByDirection.TryGetValue(side, out var list))
return System.Array.Empty<GameObject>();

var result = new List<GameObject>(list.Count);
for (int i = 0; i < list.Count; i++)
{
if (list[i] != null)
result.Add(list[i]);
}
return result.ToArray();
}

#endregion

#region Consumer API — Material swap

/// <summary>
/// Set the material on all renderers for a building part category.
/// </summary>
/// <param name="part">The building part category to modify.</param>
/// <param name="material">The material to apply.</param>
public void SetMaterial(BuildingPart part, Material material)
{
var renderers = GetRenderers(part);
for (int i = 0; i < renderers.Length; i++)
renderers[i].material = material;
}

/// <summary>
/// Set the material on all renderers for a specific wall direction.
/// </summary>
/// <param name="side">The specific wall side to modify.</param>
/// <param name="material">The material to apply.</param>
public void SetMaterial(WallSide side, Material material)
{
var renderers = GetRenderers(side);
for (int i = 0; i < renderers.Length; i++)
renderers[i].material = material;
}

/// <summary>
/// Set the material on a specific submesh index across all renderers for a building part.
/// Use submesh 0 for exterior faces, submesh 1 for interior faces on dual-material walls.
/// Renderers with fewer submeshes than <paramref name="submeshIndex"/> are skipped.
/// </summary>
/// <param name="part">The building part category to modify.</param>
/// <param name="material">The material to apply.</param>
/// <param name="submeshIndex">The submesh index to target (0 = exterior, 1 = interior).</param>
public void SetMaterial(BuildingPart part, Material material, int submeshIndex)
{
var renderers = GetRenderers(part);
SetSubmeshMaterial(renderers, material, submeshIndex);
}

/// <summary>
/// Set the material on a specific submesh index across all renderers for a wall direction.
/// Use submesh 0 for exterior faces, submesh 1 for interior faces on dual-material walls.
/// Renderers with fewer submeshes than <paramref name="submeshIndex"/> are skipped.
/// </summary>
/// <param name="side">The specific wall side to modify.</param>
/// <param name="material">The material to apply.</param>
/// <param name="submeshIndex">The submesh index to target (0 = exterior, 1 = interior).</param>
public void SetMaterial(WallSide side, Material material, int submeshIndex)
{
var renderers = GetRenderers(side);
SetSubmeshMaterial(renderers, material, submeshIndex);
}

/// <summary>
/// Set the exterior face material on all exterior wall renderers (submesh 0).
/// Only affects dual-material walls created with <see cref="Config.BuildingPalette.InteriorWallMaterial"/>.
/// Single-material walls are updated normally.
/// </summary>
/// <param name="material">The material to apply to exterior faces.</param>
public void SetExteriorWallMaterial(Material material)
{
SetMaterial(BuildingPart.ExteriorWalls, material, 0);
}

/// <summary>
/// Set the interior (room-facing) material on all exterior wall renderers (submesh 1).
/// Only affects dual-material walls. Renderers with a single material are skipped.
/// </summary>
/// <param name="material">The material to apply to interior faces.</param>
public void SetInteriorFaceMaterial(Material material)
{
SetMaterial(BuildingPart.ExteriorWalls, material, 1);
}

private static void SetSubmeshMaterial(Renderer[] renderers, Material material, int submeshIndex)
{
for (int i = 0; i < renderers.Length; i++)
{
Material[] mats = renderers[i].materials;
if (submeshIndex >= 0 && submeshIndex < mats.Length)
{
mats[submeshIndex] = material;
renderers[i].materials = mats;
}
}
}

#endregion
}
}
47 changes: 34 additions & 13 deletions Building/BuildingUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,23 +132,44 @@ public static void ApplyOcclusionSettings(GameObject gameObject, bool allowOcclu

#endregion

#region Public API - Grid Snapping
#region Public API - Grid

/// <summary>
/// Snap a position to a grid
/// Compute the grid cell size for a room with the given dimensions.
/// Returns the largest square cell size ≤ <see cref="Constants.Spatial.DefaultGridSize"/>
/// that evenly divides the room's X axis. The pathfinding grid uses this
/// same value so furniture placement and NPC navigation are always aligned.
/// </summary>
/// <param name="position">The position to snap</param>
/// <param name="gridSize">The grid cell size</param>
/// <returns>The snapped position</returns>
public static Vector3 SnapToGrid(Vector3 position, float gridSize = Constants.Spatial.DefaultGridSize)
public static float ComputeGridCellSize(float roomX, float roomZ)
{
return new Vector3(
Mathf.Round(position.x / gridSize) * gridSize,
Mathf.Round(position.y / gridSize) * gridSize,
Mathf.Round(position.z / gridSize) * gridSize
);
float target = Constants.Spatial.DefaultGridSize;
int nx = Mathf.Max(1, Mathf.CeilToInt(roomX / target));
int nz = Mathf.Max(1, Mathf.CeilToInt(roomZ / target));
return Mathf.Min(roomX / nx, roomZ / nz);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.



/// <summary>
/// Check whether a transform belongs to a living entity (player/NPC) by
/// looking for a <see cref="NavMeshAgent"/> or <see cref="CharacterController"/>
/// anywhere in its root hierarchy.
/// Results are cached in the provided sets for efficiency.
/// </summary>
/// <returns>true if the transform's root contains a NavMeshAgent or CharacterController.</returns>
internal static bool IsLivingEntity(Transform t, HashSet<int> livingRoots, HashSet<int> staticRoots)
{
int rootId = t.root.GetInstanceID();
if (livingRoots.Contains(rootId)) return true;
if (staticRoots.Contains(rootId)) return false;

Transform root = t.root;
bool isLiving = root.GetComponentInChildren<NavMeshAgent>() != null
|| root.GetComponentInChildren<CharacterController>() != null;

(isLiving ? livingRoots : staticRoots).Add(rootId);
return isLiving;
}

#endregion

#region Public API - Hierarchy Organization
Expand Down
2 changes: 1 addition & 1 deletion Building/Components/LightingBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public GameObject AddCeilingLights(float? intensity = null, Color? color = null,

float xStep = _roomSize.x / (xCount + 1);
float zStep = _roomSize.z / (zCount + 1);
float yPos = _roomSize.y - 0.2f; // Just below ceiling
float yPos = _roomSize.y - 0.3f; // Below ceiling (ceiling sits inside room at _roomSize.y - thickness)

for (int x = 1; x <= xCount; x++)
{
Expand Down
Loading