A GameResource asset class defining road appearance and behavior for a road tool. It declares enums for border and crossing styles, a RoadLineStyle helper for painted lines, and the RoadProfile resource with many editable properties, resolved materials, hashing helpers, and utility methods for border meeting rules.
using System.Text.Json.Serialization;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// What runs along the sides of a road.
/// </summary>
public enum RoadBorderStyle
{
/// <summary>
/// Bare road edge.
/// </summary>
None,
/// <summary>
/// Raised kerb and pavement (street).
/// </summary>
Sidewalk,
/// <summary>
/// A shoulder of road material that drops onto an uneven verge falling away into the terrain.
/// </summary>
CountrySide
}
/// <summary>
/// What a pedestrian crossing is painted as.
/// </summary>
public enum RoadCrossingStyle
{
/// <summary>
/// A ladder of stripes running the way traffic does, laid on the road outside the junction.
/// </summary>
Stripes,
/// <summary>
/// Two lines across the road, marking the walk over the junction's mouth: the outer one on the corner of the
/// pavement, the inner one a crossing's depth further in.
/// </summary>
Lines
}
/// <summary>
/// How one kind of painted line looks. Which lines a road gets follows from its lanes (a lane line between lanes
/// running the same way, a centre line where the directions change, edge lines along the sides); the profile
/// only says what each kind looks like.
/// </summary>
public sealed class RoadLineStyle
{
public bool Enabled { get; set; } = true;
/// <summary>
/// Paint material. Empty uses the tool's white (or yellow, for the centre line).
/// </summary>
public Material Material { get; set; }
[Range(1.0f, 50.0f)] public float Width { get; set { field = value.Clamp(0.1f, 1000.0f); } } = 8.0f;
/// <summary>
/// Painted length of each dash. 0 paints a solid line.
/// </summary>
[Range(0.0f, 2000.0f)] public float DashLength { get; set { field = value.Clamp(0.0f, 100000.0f); } } = 0.0f;
[Range(0.0f, 2000.0f)] public float GapLength { get; set { field = value.Clamp(0.0f, 100000.0f); } } = 0.0f;
/// <summary>
/// Two lines this far apart (centre to centre) instead of one. 0 paints a single line.
/// </summary>
[Range(0.0f, 100.0f)] public float DoubleSpacing { get; set { field = value.Clamp(0.0f, 1000.0f); } } = 0.0f;
[Hide, JsonIgnore] public bool IsDashed => DashLength > 0.0f && GapLength > 0.0f;
[Hide, JsonIgnore] public bool IsDouble => DoubleSpacing > 0.0f;
public void AddToHash(ref StableHash _Hash)
{
_Hash.Add(Enabled ? 1 : 0);
_Hash.Add(Material?.ResourcePath);
_Hash.Add(Width);
_Hash.Add(DashLength);
_Hash.Add(GapLength);
_Hash.Add(DoubleSpacing);
}
}
/// <summary>
/// A road type: its surface material and what its sides look like. Roads reference a profile, so every street in a
/// map changes together when the asset is edited.
/// </summary>
[AssetType(Name = "Road Profile", Extension = "road", Category = "Road Tool 2")]
public sealed class RoadProfile : GameResource
{
private static RoadProfile s_Fallback;
/// <summary>
/// What a road gets when no profile is assigned anywhere: the shipped asphalt profile, or failing that plain
/// asphalt with bare edges.
/// </summary>
public static RoadProfile Fallback => s_Fallback ??= ResourceLibrary.Get<RoadProfile>("roadtool2/resources/profiles/asphalt.road") ?? new RoadProfile();
[Group("Road")] public Material RoadMaterial { get; set; }
[Group("Road"), Range(10.0f, 10000.0f)] public float RoadTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 500.0f;
/// <summary>
/// Thins the generated geometry out where this road type hardly bends: a straight street is a couple of long
/// quads instead of one every sample. Worth leaving off for a road whose shape matters at every row (a country
/// verge wanders with noise, and rows are what it wanders at).
/// </summary>
[Group("Optimization")] public bool Simplify { get; set; } = true;
/// <summary>
/// A row where the road turns less than this may be dropped.
/// </summary>
[Group("Optimization"), ShowIf(nameof(Simplify), true), Range(0.1f, 10.0f)] public float SimplifyAngle { get; set { field = value.Clamp(0.0f, 180.0f); } } = 1.0f;
/// <summary>
/// How far the thinned road may stray from its true curve. Small keeps a gentle bend smooth; 0 drops rows on
/// the turn angle alone, which lets a long shallow curve flatten.
/// </summary>
[Group("Optimization"), ShowIf(nameof(Simplify), true), Range(0.0f, 50.0f)] public float SimplifyDeviation { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 2.0f;
/// <summary>
/// Rounds off a road of this type where it dead ends: the surface carries on past the last node as a half disc
/// and the border wraps round it, so the road reads as a turning head instead of being sliced through. Nothing
/// happens at an end another road carries on from.
/// </summary>
[Group("Road"), Title("Rounded Dead Ends")] public bool RoundedEnds { get; set; } = true;
[Group("Border")] public RoadBorderStyle Border { get; set; } = RoadBorderStyle.None;
/// <summary>
/// Pavement material for a sidewalk; verge material (grass, dirt, gravel) for a country side border.
/// </summary>
[Group("Border"), HideIf(nameof(Border), RoadBorderStyle.None)] public Material BorderMaterial { get; set; }
[Group("Border"), HideIf(nameof(Border), RoadBorderStyle.None), Range(10.0f, 10000.0f)] public float BorderTextureRepeat { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 200.0f;
/// <summary>
/// How wide one repeat of a sidewalk's texture is, across the pavement from the kerb. The texture keeps this size
/// whatever the pavement's width: a narrower pavement shows less of it, a wider one runs on into the next repeat,
/// and one tapering between the two is never squeezed or pulled to fit.
/// </summary>
[Group("Sidewalk"), ShowIf(nameof(Border), RoadBorderStyle.Sidewalk), Range(10.0f, 1000.0f)] public float BorderTextureWidth { get; set { field = value.Clamp(1.0f, 100000.0f); } } = 150.0f;
/// <summary>
/// Sidewalk: pavement width. Country side: width of the road-material shoulder before the drop.
/// </summary>
[Group("Border"), HideIf(nameof(Border), RoadBorderStyle.None), Range(0.0f, 500.0f)] public float BorderWidth { get; set { field = value.Clamp(0.0f, 5000.0f); } } = 150.0f;
[Group("Sidewalk"), ShowIf(nameof(Border), RoadBorderStyle.Sidewalk), Range(0.1f, 100.0f)] public float CurbHeight { get; set { field = value.Clamp(0.1f, 1000.0f); } } = 5.0f;
/// <summary>
/// Pushes the top of the kerb outward, turning the vertical kerb face into a slope. 0 is a vertical kerb.
/// </summary>
[Group("Sidewalk"), ShowIf(nameof(Border), RoadBorderStyle.Sidewalk), Range(0.0f, 100.0f)] public float CurbBevel { get; set { field = value.Clamp(0.0f, 1000.0f); } } = 0.0f;
/// <summary>
/// How far the verge steps down from the shoulder: the exposed thickness of the road.
/// </summary>
[Group("Country Side"), ShowIf(nameof(Border), RoadBorderStyle.CountrySide), Range(0.0f, 100.0f)] public float Drop { get; set { field = value.Clamp(0.0f, 1000.0f); } } = 8.0f;
[Group("Country Side"), ShowIf(nameof(Border), RoadBorderStyle.CountrySide), Range(10.0f, 1000.0f)] public float VergeWidth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 200.0f;
[Group("Country Side"), ShowIf(nameof(Border), RoadBorderStyle.CountrySide), Range(0.0f, 500.0f)] public float VergeDepth { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 60.0f;
[Group("Country Side"), ShowIf(nameof(Border), RoadBorderStyle.CountrySide), Range(1, 12)] public int SlopeSegments { get; set { field = value.Clamp(1, 32); } } = 3;
/// <summary>
/// How far the shoulder and verge edges wander, and how much the slope rolls, in units.
/// </summary>
[Group("Country Side"), ShowIf(nameof(Border), RoadBorderStyle.CountrySide), Range(0.0f, 200.0f)] public float Chaos { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 30.0f;
/// <summary>
/// Frequency of the wander: smaller values give longer, gentler waves.
/// </summary>
[Group("Country Side"), ShowIf(nameof(Border), RoadBorderStyle.CountrySide), Range(0.0f, 0.2f)] public float ChaosScale { get; set { field = value.Clamp(0.00001f, 10.0f); } } = 0.02f;
/// <summary>
/// Where this road type meets another at a pass-through node, its border tapers into the other's over this
/// length: a kerb sinks to the road, a verge flattens out, the widths meet, so the change of road type is a
/// slope rather than a step.
/// </summary>
[Group("Border"), HideIf(nameof(Border), RoadBorderStyle.None), Range(0.0f, 2000.0f)] public float TransitionLength { get; set { field = value.Clamp(0.0f, 100000.0f); } } = 300.0f;
/// <summary>
/// The material a sidewalk's pavement takes over the transition into a country road, so the verge's gravel
/// can be seen creeping onto it: a "blendable" material whose first layer is the pavement and whose second is
/// the gravel, mixed per vertex (solid gravel at the node, pavement again by the end of the transition, with a
/// ragged edge between). Empty keeps the pavement plain up to the node.
/// </summary>
[Group("Sidewalk"), ShowIf(nameof(Border), RoadBorderStyle.Sidewalk)] public Material BlendMaterial { get; set; }
/// <summary>
/// Between two lanes running the same way.
/// </summary>
[Group("Markings")] public RoadLineStyle LaneLine { get; set; } = new() { DashLength = 200.0f, GapLength = 300.0f };
/// <summary>
/// Between the two directions of a two-way road.
/// </summary>
[Group("Markings")] public RoadLineStyle CentreLine { get; set; } = new();
/// <summary>
/// Along both sides of the road.
/// </summary>
[Group("Markings")] public RoadLineStyle EdgeLine { get; set; } = new() { Enabled = false, Width = 6.0f };
/// <summary>
/// How far in from the road's edge the edge line sits (to its outer side). A wide margin makes a cycle lane
/// or a hard shoulder.
/// </summary>
[Group("Markings"), Range(-100.0f, 100.0f)] public float EdgeLineMargin { get; set { field = value.Clamp(-1000.0f, 1000.0f); } } = -10.0f;
/// <summary>
/// Pedestrian crossings across every arm of a junction, just outside the junction: a row of painted stripes
/// laid on the road, as many as its width has room for. Roads can still turn theirs off end by end.
/// </summary>
[Group("Crossings")] public bool Crossings { get; set; } = false;
/// <summary>
/// What a crossing looks like: a ladder of stripes laid outside the junction, or the two lines across the
/// junction's mouth used in much of the US.
/// </summary>
[Group("Crossings"), ShowIf(nameof(Crossings), true), Title("Style")] public RoadCrossingStyle CrossingStyle { get; set; } = RoadCrossingStyle.Stripes;
/// <summary>
/// The paint a crossing's stripes are made of. Empty uses the tool's own.
/// </summary>
[Group("Crossings"), ShowIf(nameof(Crossings), true)] public Material CrossingMaterial { get; set; }
/// <summary>
/// How far along the road the crossing reaches: the length of each of its stripes.
/// </summary>
[Group("Crossings"), ShowIf(nameof(Crossings), true), Range(20.0f, 500.0f)] public float CrossingDepth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 120.0f;
/// <summary>
/// How wide one stripe of a crossing is, across the road; in the two-line style, how thick each line is.
/// </summary>
[Group("Crossings"), ShowIf(nameof(Crossings), true), Range(5.0f, 200.0f)] public float CrossingStripeWidth { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 40.0f;
/// <summary>
/// The gap left between one stripe and the next. How many stripes a crossing gets follows from this and the
/// width of the road it is painted on.
/// </summary>
[Group("Crossings"), ShowIf(nameof(Crossings), true), Range(5.0f, 200.0f)] public float CrossingStripeGap { get; set { field = value.Clamp(1.0f, 10000.0f); } } = 40.0f;
/// <summary>
/// Gap between the crossing and the road's edges on each side.
/// </summary>
[Group("Crossings"), ShowIf(nameof(Crossings), true), Range(0.0f, 100.0f)] public float CrossingInset { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 10.0f;
/// <summary>
/// Gap the painted lines keep from a crossing.
/// </summary>
[Group("Crossings"), ShowIf(nameof(Crossings), true), Range(0.0f, 300.0f)] public float CrossingClearance { get; set { field = value.Clamp(0.0f, 10000.0f); } } = 40.0f;
[Hide, JsonIgnore] public bool HasBorder => Border != RoadBorderStyle.None;
/// <summary>
/// How tall this road type's border stands at the road's edge, which is what the relief of a transition scales: a
/// sidewalk's kerb, a country road's drop and the verge falling away below it.
/// </summary>
[Hide, JsonIgnore] public float ReliefHeight => Border switch
{
RoadBorderStyle.Sidewalk => CurbHeight,
RoadBorderStyle.CountrySide => Drop + VergeDepth,
_ => 0.0f
};
/// <summary>
/// The width BOTH borders run to where two road types meet, so they line up there: running each one to the
/// other's width only swaps them over and leaves a step of the whole difference between the two. A kerb is a
/// built thing and keeps the width it has, so a verge on the other side is what gives; two borders of a kind
/// meet halfway; and a border with nothing on the other side still runs out to nothing.
/// </summary>
public static float MeetingWidth(RoadProfile _Profile, RoadProfile _Other)
{
if (!_Profile.HasBorder || !_Other.HasBorder)
return 0.0f;
if (_Profile.Border == _Other.Border)
return (_Profile.FlatBorderWidth + _Other.FlatBorderWidth) * 0.5f;
return _Profile.Border == RoadBorderStyle.Sidewalk ? _Profile.FlatBorderWidth : _Other.FlatBorderWidth;
}
/// <summary>
/// The share of its own relief a border runs to where it meets another road type's. A different kind of border
/// on the other side has only the road itself in common with it, so both flatten into that. Two of a kind meet
/// halfway in height as they do in width, so a low kerb running into a high one ramps up and the high one down,
/// instead of the two standing side by side with a step between them.
/// </summary>
public static float MeetingRelief(RoadProfile _Profile, RoadProfile _Other)
{
if (!_Other.HasBorder || _Other.Border != _Profile.Border)
return 0.0f;
float own = _Profile.ReliefHeight;
if (own <= 0.001f)
return 1.0f;
return (own + _Other.ReliefHeight) * 0.5f / own;
}
/// <summary>
/// How this road type's rows are thinned out, as the row sampler wants it. A verge that wanders with noise caps
/// how far apart the rows may be spread, because the sampler decides what it can drop by measuring the ROAD'S
/// EDGES and knows nothing about a border that moves between them: left to itself it drops every row on a
/// straight road and leaves the verge a run of long flat facets stepping past each other. One feature of that
/// noise is 1 / <see cref="ChaosScale"/> across, and nothing that fine survives rows spread wider than it — so
/// a coarser <see cref="ChaosScale"/> (or no <see cref="Chaos"/> at all) is what buys a country road its
/// thinning back.
/// </summary>
[Hide, JsonIgnore] public RoadSimplify Simplification => new(Simplify, SimplifyAngle, SimplifyDeviation, BorderWanders ? 1.0f / ChaosScale : 0.0f);
/// <summary>
/// Whether this road type's border has shape of its own between the rows, rather than following the road.
/// </summary>
[Hide, JsonIgnore] public bool BorderWanders => Border == RoadBorderStyle.CountrySide && Chaos > 0.0f && ChaosScale > 0.0f;
[Hide, JsonIgnore] public Material ResolvedCrossingMaterial => CrossingMaterial ?? Material.Load("roadtool2/materials/crossingstrip.vmat");
/// <summary>
/// Folds the values the crossings are built from into a hash.
/// </summary>
public void AddCrossingsToHash(ref StableHash _Hash)
{
_Hash.Add(Crossings ? 1 : 0);
_Hash.Add((int)CrossingStyle);
_Hash.Add(CrossingMaterial?.ResourcePath);
_Hash.Add(CrossingDepth);
_Hash.Add(CrossingStripeWidth);
_Hash.Add(CrossingStripeGap);
_Hash.Add(CrossingInset);
_Hash.Add(CrossingClearance);
}
/// <summary>
/// The material lists use "road" for surfaces that extend the road and "border" for the rest.
/// </summary>
[Hide, JsonIgnore] public Material ResolvedRoadMaterial => RoadMaterial ?? Material.Load("roadtool2/materials/asphalt.vmat");
[Hide, JsonIgnore] public Material ResolvedBorderMaterial => BorderMaterial ?? Material.Load("roadtool2/materials/sidewalk.vmat");
[Hide, JsonIgnore] public Material ResolvedLaneLineMaterial => LaneLine?.Material ?? Material.Load("roadtool2/materials/linewhite.vmat");
[Hide, JsonIgnore] public Material ResolvedCentreLineMaterial => CentreLine?.Material ?? Material.Load("roadtool2/materials/lineyellow.vmat");
[Hide, JsonIgnore] public Material ResolvedEdgeLineMaterial => EdgeLine?.Material ?? Material.Load("roadtool2/materials/linewhite.vmat");
/// <summary>
/// Folds the values the painted lines are built from into a hash.
/// </summary>
public void AddMarkingsToHash(ref StableHash _Hash)
{
foreach (var style in new[] { LaneLine, CentreLine, EdgeLine })
{
_Hash.Add(style is null ? 0 : 1);
style?.AddToHash(ref _Hash);
}
_Hash.Add(EdgeLineMargin);
}
/// <summary>
/// Folds the values the road surface is built from into a hash, so pieces built with an older version of the
/// profile are noticed.
/// </summary>
public void AddSurfaceToHash(ref StableHash _Hash)
{
_Hash.Add(RoadMaterial?.ResourcePath);
_Hash.Add(RoadTextureRepeat);
// Where the rows sit is what the surface, the borders and the bridge are all built on, and every one of
// their signatures folds this one in.
Simplification.AddToHash(ref _Hash);
}
/// <summary>
/// Folds the values the borders are built from into a hash.
/// </summary>
public void AddBorderToHash(ref StableHash _Hash)
{
_Hash.Add((int)Border);
_Hash.Add(BorderMaterial?.ResourcePath);
_Hash.Add(BorderTextureRepeat);
_Hash.Add(BorderTextureWidth);
_Hash.Add(BorderWidth);
_Hash.Add(CurbHeight);
_Hash.Add(CurbBevel);
_Hash.Add(Drop);
_Hash.Add(VergeWidth);
_Hash.Add(VergeDepth);
_Hash.Add(SlopeSegments);
_Hash.Add(Chaos);
_Hash.Add(ChaosScale);
_Hash.Add(TransitionLength);
_Hash.Add(BlendMaterial?.ResourcePath);
}
/// <summary>
/// The width of this road type's border when flattened to the road: what a neighbouring road type's border
/// tapers to where the two meet.
/// </summary>
[Hide, JsonIgnore]
public float FlatBorderWidth => Border switch
{
RoadBorderStyle.Sidewalk => BorderWidth,
RoadBorderStyle.CountrySide => BorderWidth + VergeWidth,
_ => 0.0f
};
/// <summary>
/// The asset was saved: every network using it has stale pieces now.
/// </summary>
protected override void PostReload()
{
base.PostReload();
RoadNetwork.Editor_MarkDirty();
}
protected override Bitmap CreateAssetTypeIcon(int _Width, int _Height)
{
return CreateSimpleAssetTypeIcon("add_road", _Width, _Height, "#ffb347", "black");
}
}