Profiles/RoadPropSet.cs

Asset type defining props placed along roads and at junctions. Contains data classes for rows along roads (RoadPropRow) and junction props (RoadJunctionProp), plus a RoadPropSet resource that holds lists of those and produces a stable hash and an asset key for prefabs.

File Access
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Which side of a road something stands on, looking along the road (start to end) for props along it, and
/// looking into the junction for props at one.
/// </summary>
public enum RoadSide
{
	Both,
	Left,
	Right
}

/// <summary>
/// The line a prop's lateral offset is measured from.
/// </summary>
public enum PropAnchor
{
	/// <summary>
	/// The edge of the road surface.
	/// </summary>
	RoadEdge,

	/// <summary>
	/// The outer edge of the road's border (the back of the pavement, the far side of a verge); the road edge
	/// itself for a road with no border.
	/// </summary>
	BorderEdge
}

/// <summary>
/// One kind of prop repeated along a road: a prefab at even intervals, on one or both sides, at a set distance
/// from the road or its border. Two rows with the same spacing and different phases alternate (lamp posts with a
/// tree halfway between each pair).
/// </summary>
public sealed class RoadPropRow
{
	/// <summary>
	/// Placed with its forward along the road and its right pointing away from the road.
	/// </summary>
	public GameObject Prefab { get; set; }

	public RoadSide Side { get; set; } = RoadSide.Both;

	/// <summary>
	/// Distance between two of them. The road is divided into as many equal bays as come nearest to it.
	/// </summary>
	[Range(50.0f, 5000.0f)] public float Spacing { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 1000.0f;

	/// <summary>
	/// Where in each bay the prop stands, as a fraction of the spacing: 0 at the bay's start, 0.5 halfway along.
	/// </summary>
	[Range(0.0f, 1.0f)] public float Phase { get; set { field = value.Clamp(0.0f, 0.999f); } } = 0.5f;

	public PropAnchor Anchor { get; set; } = PropAnchor.BorderEdge;

	/// <summary>
	/// How far out from the anchor line it stands; negative brings it back towards the road.
	/// </summary>
	[Range(-500.0f, 500.0f)] public float Offset { get; set { field = value.Clamp(-100000.0f, 100000.0f); } } = -30.0f;

	/// <summary>
	/// Raised (or sunk) this much from where it lands: on the border within it, on the ground beyond it.
	/// </summary>
	[Range(-100.0f, 100.0f)] public float HeightOffset { get; set { field = value.Clamp(-100000.0f, 100000.0f); } } = 0.0f;

	/// <summary>
	/// Turned this much from facing along the road.
	/// </summary>
	[Range(-180.0f, 180.0f)] public float Yaw { get; set; } = 0.0f;

	/// <summary>
	/// The share of places that actually get one; the rest are left empty, at random but always the same way.
	/// </summary>
	[Group("Variation"), Range(0.0f, 1.0f)] public float Chance { get; set { field = value.Clamp(0.0f, 1.0f); } } = 1.0f;

	/// <summary>
	/// Each one turned a random way round (trees, rocks).
	/// </summary>
	[Group("Variation")] public bool RandomYaw { get; set; } = false;

	/// <summary>
	/// Each one shifted up to this far from its place, along and across.
	/// </summary>
	[Group("Variation"), Range(0.0f, 500.0f)] public float Scatter { get; set { field = value.Clamp(0.0f, 100000.0f); } } = 0.0f;

	[Group("Variation"), Range(0.1f, 5.0f)] public float MinScale { get; set { field = value.Clamp(0.01f, 100.0f); } } = 1.0f;
	[Group("Variation"), Range(0.1f, 5.0f)] public float MaxScale { get; set { field = value.Clamp(0.01f, 100.0f); } } = 1.0f;



	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add(RoadPropSet.PrefabKey(Prefab));
		_Hash.Add((int)Side);
		_Hash.Add(Spacing);
		_Hash.Add(Phase);
		_Hash.Add((int)Anchor);
		_Hash.Add(Offset);
		_Hash.Add(HeightOffset);
		_Hash.Add(Yaw);
		_Hash.Add(Chance);
		_Hash.Add(RandomYaw ? 1 : 0);
		_Hash.Add(Scatter);
		_Hash.Add(MinScale);
		_Hash.Add(MaxScale);
	}
}

/// <summary>
/// One kind of prop standing near a junction, a set distance before it along each road that meets it: a fire
/// hydrant, a post box, a sign.
/// </summary>
public sealed class RoadJunctionProp
{
	/// <summary>
	/// Placed with its forward towards the junction and its right pointing away from the road.
	/// </summary>
	public GameObject Prefab { get; set; }

	/// <summary>
	/// Looking into the junction. With right-hand traffic the right side is the one the traffic arrives on.
	/// </summary>
	public RoadSide Side { get; set; } = RoadSide.Right;

	/// <summary>
	/// How far back from the junction's edge it stands.
	/// </summary>
	[Range(0.0f, 2000.0f)] public float Distance { get; set { field = value.Clamp(0.0f, 100000.0f); } } = 150.0f;

	public PropAnchor Anchor { get; set; } = PropAnchor.BorderEdge;

	[Range(-500.0f, 500.0f)] public float Offset { get; set { field = value.Clamp(-100000.0f, 100000.0f); } } = -30.0f;

	[Range(-100.0f, 100.0f)] public float HeightOffset { get; set { field = value.Clamp(-100000.0f, 100000.0f); } } = 0.0f;

	[Range(-180.0f, 180.0f)] public float Yaw { get; set; } = 0.0f;

	[Range(0.0f, 1.0f)] public float Chance { get; set { field = value.Clamp(0.0f, 1.0f); } } = 1.0f;



	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add(RoadPropSet.PrefabKey(Prefab));
		_Hash.Add((int)Side);
		_Hash.Add(Distance);
		_Hash.Add((int)Anchor);
		_Hash.Add(Offset);
		_Hash.Add(HeightOffset);
		_Hash.Add(Yaw);
		_Hash.Add(Chance);
	}
}

/// <summary>
/// What stands along a road and near its junctions: rows of prefabs repeated along it (lamp posts, trees, bollards,
/// guard rails) and prefabs placed a set distance before each junction (hydrants, post boxes, signs). Roads reference
/// a set, so a street's furniture changes together when the asset is edited.
/// </summary>
[AssetType(Name = "Road Props", Extension = "props", Category = "Road Tool 2")]
public sealed class RoadPropSet : GameResource
{
	[Group("Along The Road")] public List<RoadPropRow> Rows { get; set; } = new();

	[Group("At Junctions")] public List<RoadJunctionProp> JunctionRows { get; set; } = new();

	/// <summary>
	/// Changes which places the chance, scatter and random turns pick, everywhere this set is used.
	/// </summary>
	[Group("Variation")] public int Seed { get; set; } = 0;

	[Hide, JsonIgnore] public bool IsEmpty => (Rows is null || Rows.Count == 0) && (JunctionRows is null || JunctionRows.Count == 0);



	public void AddToHash(ref StableHash _Hash)
	{
		_Hash.Add(Seed);
		_Hash.Add(Rows?.Count ?? 0);

		if (Rows is not null)
		{
			foreach (var row in Rows)
				row.AddToHash(ref _Hash);
		}

		_Hash.Add(JunctionRows?.Count ?? 0);

		if (JunctionRows is not null)
		{
			foreach (var row in JunctionRows)
				row.AddToHash(ref _Hash);
		}
	}



	/// <summary>
	/// A prefab property holds the prefab's scene; its asset path is what tells one prefab from another.
	/// </summary>
	public static string PrefabKey(GameObject _Prefab)
	{
		if (!_Prefab.IsValid())
			return null;

		if (_Prefab is PrefabScene scene)
			return scene.Source?.ResourcePath ?? scene.Name;

		return _Prefab.PrefabInstanceSource ?? _Prefab.Name;
	}



	/// <summary>
	/// The asset was saved: every network with roads using this set has stale pieces now.
	/// </summary>
	protected override void PostReload()
	{
		base.PostReload();

		RoadNetwork.Editor_MarkDirty();
	}



	protected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)
	{
		return CreateSimpleAssetTypeIcon("light", _Width, _Height, "#ffd866", "black");
	}
}