Map/MansionMapInstance.cs

Map instance class for the Mansion level. It handles creating and wiring map-placed entities (player spawns, doors, loot spawners, shop, signs, etc.) when map objects are instantiated, adding components, models, tags and network-spawning host-owned objects.

NetworkingFile Access
using Sandbox;

namespace BrickJam;

public sealed partial class MansionMapInstance : MapInstance
{
	// NPC navigation now uses the GridAStar grid (built per-level in Level.GenerateGrid), not the scene
	// NavMesh, so the old OnMapLoaded navmesh-rebuild hook has been removed.

	protected override void OnCreateObject( GameObject go, MapLoader.ObjectEntry kv )
	{
		if ( !Game.IsPlaying ) return;

		if ( !Networking.IsHost ) return;
		
		switch ( kv.TypeName?.ToLowerInvariant() )
		{
			case "playerspawn":
			case "player_spawn":
				CreatePlayerSpawn( go, kv );
				break;
			case "validtrapdoorposition":
			case "valid_trapdoor_position":
				CreateValidTrapdoorPosition( go, kv );
				break;
			case "validfinaldoorposition":
			case "valid_final_door_position":
				go.Components.Create<ValidFinalDoorPosition>();
				break;
			case "lootspawner":
			case "loot_spawner":
				CreateLootSpawner( go, kv );
				break;
			case "pissingguyspawner":
			case "pissing_guy_spawner":
				CreatePissingGuySpawner( go, kv );
				break;
			case "door":
				CreateDoor( go, kv );
				break;
			case "shopdoor":
			case "shop_door":
				CreateShopDoor( go );
				break;
			case "shop":
				CreateShop( go );
				break;
			case "slipperysign":
			case "slippery_sign":
				CreateSlipperySign( go );
				break;
			case "sellarea":
			case "sell_area":
				CreateSellArea( go );
				break;
			default:
				// Surface anything we didn't handle so unmapped legacy Hammer types are obvious.
				if ( !string.IsNullOrWhiteSpace( kv.TypeName ) )
					Log.Info( $"[MansionMapInstance] Unhandled map entity type: '{kv.TypeName}'" );
				break;
		}
	}

	private static void CreatePlayerSpawn( GameObject go, MapLoader.ObjectEntry kv )
	{
		var component = go.Components.Create<PlayerSpawn>();
		component.LevelType = kv.GetValue( "LevelType", LevelType.None );
	}

	private static void CreateValidTrapdoorPosition( GameObject go, MapLoader.ObjectEntry kv )
	{
		var component = go.Components.Create<ValidTrapdoorPosition>();
		component.LevelType = kv.GetValue( "LevelType", LevelType.None );
	}

	private static void CreateLootSpawner( GameObject go, MapLoader.ObjectEntry kv )
	{
		var component = go.Components.Create<LootSpawner>();
		component.LootToSpawn = ResolveLootPrefab( kv );
		component.IsContainer = kv.GetValue( "IsContainer", false );
		component.ChanceToSpawn = kv.GetValue( "ChanceToSpawn", 0.5f );
		component.LevelType = kv.GetValue( "LevelType", LevelType.None );

		Log.Info( $"[MansionMapInstance] LootSpawner: level={component.LevelType} container={component.IsContainer} " +
			$"chance={component.ChanceToSpawn} loot={component.LootToSpawn?.ResourceName ?? "<none>"}" );
	}

	/// <summary>
	/// Resolve the loot resource referenced by a spawner. Tries the direct resource path first,
	/// then falls back to looking it up by file name via the <see cref="LootPrefab"/> registry
	/// (handles path-format differences between the legacy map data and the current project layout).
	/// </summary>
	private static LootPrefab ResolveLootPrefab( MapLoader.ObjectEntry kv )
	{
		var loot = kv.GetResource<LootPrefab>( "LootToSpawn" );
		if ( loot is not null )
			return loot;

		var raw = kv.GetString( "LootToSpawn" );
		if ( string.IsNullOrWhiteSpace( raw ) )
			return null;

		return LootPrefab.Get( System.IO.Path.GetFileNameWithoutExtension( raw ) );
	}

	private static void CreatePissingGuySpawner( GameObject go, MapLoader.ObjectEntry kv )
	{
		var component = go.Components.Create<PissingGuySpawner>();
		component.ChanceToSpawn = kv.GetValue( "ChanceToSpawn", 0.35f );
		AddModel( go, "models/pissing_guy/pissing_guy.vmdl", false );
	}

	private static void CreateDoor( GameObject go, MapLoader.ObjectEntry kv )
	{
		// OnCreateObject is host-only (guard at the top of the override), so the door is built once on
		// the host and replicated to every client via NetworkSpawn. This is essential: a client's
		// RequestUse ([Rpc.Host]) only ever swings the HOST's door, so it MUST be one shared networked
		// object - otherwise a client's own local copy would stay shut ("clients can't open doors").
		var component = go.Components.Create<Door>();
		component.LevelType = kv.GetValue( "LevelType", LevelType.None );
		component.AlternativeModel = kv.GetValue( "AlternativeModel", false );
		component.Locked = kv.GetValue( "Locked", false );

		AddModel( go, GetDoorModel( component.LevelType, component.AlternativeModel ), true );
		go.Tags.Add( "door" );
		go.NetworkSpawn();
	}

	private static void CreateShopDoor( GameObject go )
	{
		go.Components.Create<ShopDoor>();
		AddModel( go, "models/furniture/mansion_furniture/mansion_door.vmdl", true );
		go.Tags.Add( "door" );
		// Host-only creation (see OnCreateObject), so network it or clients can't see/use the entrance.
		go.NetworkSpawn();
	}

	private static void CreateShop( GameObject go )
	{
		go.Components.Create<Shop>();
		AddModel( go, "models/furniture/shop/shop_counter.vmdl", true );
		go.Tags.Add( "solid", "shop" );
		// Network it: clients need the counter's "solid" collision and a usable target to aim at.
		go.NetworkSpawn();
	}

	private static void CreateSlipperySign( GameObject go )
	{
		go.Components.Create<SlipperySign>();
		AddModel( go, "models/furniture/bathrooms_furniture/wet_floor_sign.vmdl", true );
		go.Tags.Add( "slip", "nocollide" );
		// Network it: each client's owner-side CalculateSlip traces the "slip" tag locally.
		go.NetworkSpawn();
	}

	private static void CreateSellArea( GameObject go )
	{
		go.Components.Create<SellArea>();
		go.Tags.Add( "sell_area" );
	}

	private static string GetDoorModel( LevelType levelType, bool alternativeModel )
	{
		return levelType switch
		{
			LevelType.Dungeon => alternativeModel
				? "models/furniture/dungeon_props/dungeon_jail_door.vmdl"
				: "models/furniture/dungeon_props/dungeon_wood_door.vmdl",
			LevelType.Bathrooms => "models/furniture/dungeon_props/bathroom_stall_door.vmdl",
			_ => "models/furniture/mansion_furniture/mansion_door.vmdl",
		};
	}

	private static void AddModel( GameObject go, string modelPath, bool withCollider )
	{
		var model = Model.Load( modelPath );
		if ( model is null )
			return;

		var renderer = go.Components.Create<SkinnedModelRenderer>();
		renderer.Model = model;

		if ( !withCollider )
			return;

		var collider = go.Components.Create<ModelCollider>();
		collider.Model = model;
	}
}