Editor tool and sidebar UI for the Sea feature of a water system. WaterSeaTool manages detection, building and clearing of coastline data in the WaterNetwork, draws gizmos for coastlines each frame, and exposes actions; WaterSeaToolWidget is the editor sidebar that shows and edits WaterNetwork properties, tracks undo scopes, and updates stats.
using System;
using System.Linq;
using Editor;
using Sandbox;
namespace RedSnail.WaterTool2.Editor;
/// <summary>
/// The sea: its level and waves, and the coast, built at the press of a button by scanning the terrain (and the
/// colliders or models the coast is detected from) for where the land rises out of the sea. Round the land, out to the
/// coast distance, the coast zone is the sea's water as a surface of its own, which rivers open into as they open into a
/// lake; past it the infinite sea takes over, masked out over the zone and the land. Rivers are opened into the sea with
/// the River tool (click on the sea) or the Edit tool (Join Sea).
/// </summary>
[Title("Sea")]
[Icon("sailing")]
[Group("1")]
[Order(3)]
public class WaterSeaTool : EditorTool
{
/// <summary>
/// Bumped whenever the sidebar has something new to show.
/// </summary>
public int Version { get; private set; }
public WaterNetwork Network => Root?.Network;
private WaterNetworkTool Root => Manager?.CurrentTool as WaterNetworkTool;
// What the sidebar last showed, so it rebuilds when that changes underneath it (undo, the inspector).
private WaterNetwork m_ShownNetwork;
private int m_ShownCoastlines = -1;
private bool m_ShownStale;
private string m_ShownReport;
public override Widget CreateToolSidebar()
{
return new WaterSeaToolWidget(this);
}
public override void OnUpdate()
{
var network = Network;
int coastlines = network.IsValid() ? network.Lakes.Count(lake => lake.IsCoast) : -1;
bool stale = network.IsValid() && network.IsCoastStale;
string report = network.IsValid() ? network.CoastReport : null;
if (network != m_ShownNetwork || coastlines != m_ShownCoastlines || stale != m_ShownStale || report != m_ShownReport)
{
m_ShownNetwork = network;
m_ShownCoastlines = coastlines;
m_ShownStale = stale;
m_ShownReport = report;
Version++;
}
if (network.IsValid())
DrawCoast(network);
}
/// <summary>
/// Scans the scene and lays the coast round the land, creating the network first when the scene has none.
/// </summary>
public void BuildCoast()
{
var root = Root;
if (root is null)
return;
var network = root.Network;
bool createNetwork = !network.IsValid();
var scope = SceneEditorSession.Active.UndoScope("Build Coast");
if (createNetwork)
scope.WithGameObjectCreations();
else
scope.WithComponentChanges(network);
using (scope.Push())
{
if (createNetwork)
network = root.CreateNetwork();
network.BuildCoast();
}
Version++;
}
public void ClearCoast()
{
var network = Network;
if (!network.IsValid())
return;
using (SceneEditorSession.Active.UndoScope("Clear Coast").WithComponentChanges(network).Push())
network.ClearCoast();
Version++;
}
/// <summary>
/// The coastlines, where the coast zone gives way to the infinite sea, and the blocks river mouths are built in.
/// </summary>
private static void DrawCoast(WaterNetwork _Network)
{
var transform = _Network.WorldTransform;
using (Gizmo.Scope("watertool2_sea"))
{
Gizmo.Draw.IgnoreDepth = true;
Gizmo.Draw.LineThickness = 2.0f;
Gizmo.Draw.Color = Color.Blue.WithAlpha(0.9f);
foreach (var lake in _Network.Lakes)
{
if (lake.IsCoast)
WaterGizmos.DrawLake(_Network, lake, transform);
}
Gizmo.Draw.LineThickness = 1.0f;
Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.5f);
foreach (var (a, b) in _Network.CoastZoneOutline())
Gizmo.Draw.Line(a, b);
Gizmo.Draw.Color = Color.Orange.WithAlpha(0.6f);
foreach (var block in _Network.RiverMouthBlocks())
Gizmo.Draw.LineBBox(block);
}
}
}
file class WaterSeaToolWidget : ToolSidebarWidget
{
private readonly WaterSeaTool m_Tool;
private int m_ShownVersion = -1;
private IDisposable m_UndoScope;
private Label m_Stats;
public WaterSeaToolWidget(WaterSeaTool _Tool)
{
m_Tool = _Tool;
Rebuild();
}
[EditorEvent.Frame]
public void Frame()
{
if (m_Tool.Version != m_ShownVersion)
Rebuild();
// Kept up to date in place: rebuilding the sidebar under a property being dragged would drop the drag.
var network = m_Tool.Network;
if (m_Stats.IsValid() && network.IsValid())
{
string stats = network.SeaStats;
if (m_Stats.Text != stats)
m_Stats.Text = stats;
}
}
private void Rebuild()
{
m_ShownVersion = m_Tool.Version;
Layout.Clear(true);
m_Stats = null;
AddTitle("Sea", "sailing");
var network = m_Tool.Network;
if (!network.IsValid())
{
var start = AddGroup("Sea");
start.Add(new Label("Build the coast to start: the terrain is scanned, a coastline laid round the land, and the sea turned on.") { WordWrap = true });
start.Add(new Button("Build Coast", "landscape") { Clicked = m_Tool.BuildCoast });
Layout.AddStretchCell();
return;
}
var serialized = network.GetSerialized();
serialized.OnPropertyStartEdit += _ => BeginEdit("Edit Sea");
serialized.OnPropertyFinishEdit += _ => EndEdit();
var sea = AddGroup("Sea");
SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaEnabled)));
SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaLevel)));
SidebarRows.AddWide(sea, serialized.GetProperty(nameof(WaterNetwork.SeaProfile)));
SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaWaves)));
SidebarRows.Add(sea, serialized.GetProperty(nameof(WaterNetwork.SeaDepth)));
var coast = AddGroup("Coast");
SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastDetection)));
SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastResolution)));
SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastHeightMargin)));
SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.CoastDistance)));
SidebarRows.Add(coast, serialized.GetProperty(nameof(WaterNetwork.EstuarySize)));
SidebarRows.AddWide(coast, serialized.GetProperty(nameof(WaterNetwork.CoastIgnoreTags)));
var buttons = coast.AddRow();
buttons.Spacing = 4;
buttons.Add(new Button("Build Coast", "landscape") { Clicked = m_Tool.BuildCoast, ToolTip = "Scan the terrain and lay the coast round the land, in place of the one there is." });
buttons.Add(new Button("Clear", "delete") { Clicked = m_Tool.ClearCoast, ToolTip = "Take the coast away: the infinite sea is everywhere again." });
int coastlines = network.Lakes.Count(lake => lake.IsCoast);
coast.Add(new Label(coastlines == 0 ? "No coast yet: the infinite sea is everywhere." : $"Coastlines: {coastlines}") { WordWrap = true });
if (!string.IsNullOrEmpty(network.CoastReport))
coast.Add(new Label(network.CoastReport) { WordWrap = true });
if (network.IsCoastStale)
coast.Add(new Label("The sea level has moved since the coast was built: build it again.") { WordWrap = true });
var horizon = AddGroup("Infinite Sea");
SidebarRows.Add(horizon, serialized.GetProperty(nameof(WaterNetwork.SeaCellSize)));
SidebarRows.Add(horizon, serialized.GetProperty(nameof(WaterNetwork.SeaRings)));
SidebarRows.Add(horizon, serialized.GetProperty(nameof(WaterNetwork.SeaRingCells)));
var drawing = AddGroup("Drawing");
m_Stats = drawing.Add(new Label(network.SeaStats) { WordWrap = true });
AddShortcuts(
("Open A River Into The Sea", "River tool: LMB on sea"),
("Open A River End Into The Sea", "Edit tool: Join Sea")
);
Layout.AddStretchCell();
}
private void BeginEdit(string _Name)
{
var network = m_Tool.Network;
if (!network.IsValid())
return;
m_UndoScope ??= SceneEditorSession.Active.UndoScope(_Name).WithComponentChanges(network).Push();
}
private void EndEdit()
{
m_UndoScope?.Dispose();
m_UndoScope = null;
}
}