Editor/ApexWorldEditorWindow.cs
using Editor;
using Sandbox;
using Sandbox.Core;
using Sandbox.Spawners;
using System;
using System.Linq;
using Sandbox.UI;
using Button = Editor.Button;
using Label = Editor.Label;
[EditorApp( "Apex World", "landscape", "Apex World terrain generation tools" )]
public class ApexWorldEditorWindow : Widget
{
#region Fields
private ApexWorldManager _manager;
private Label _statusLabel;
private Widget _tabConfigs;
private Widget _tabTerrains;
private Widget _tabSpawners;
private Button _btnConfigs;
private Button _btnTerrains;
private Button _btnSpawners;
private Widget _spawnerListContainer;
private Widget _terrainListContainer;
private Widget _createManagerBtnContainer;
private enum SpawnerCreateType { TextureSpawner, DetailSpawner }
private SpawnerCreateType _newSpawnerType = SpawnerCreateType.TextureSpawner;
private bool _comboInitialized;
private Widget _configListContainer;
private const string StyleTabActive = "background-color: #2d4a3a; color: #90ff90; border-bottom: 3px solid #70d090; border-radius: 0; padding: 10px 0; font-weight: bold; font-size: 13px;";
private const string StyleTabInactive = "background-color: transparent; color: #6677aa; border-bottom: 3px solid transparent; border-radius: 0; padding: 10px 0; font-size: 13px;";
private const string StyleRow = "background-color: #1e2535; border-bottom: 1px solid #252c3a; padding: 6px 0;";
private const string StyleBtnGreen = "background-color: #1e3828; color: #70d090; border: 1px solid #2d5a3d; border-radius: 3px; padding: 8px 14px; font-size: 12px; font-weight: 600;";
private const string StyleBtnRed = "background-color: #2e1c1c; color: #d07070; border: 1px solid #4a2828; border-radius: 3px; padding: 8px 14px; font-size: 12px; font-weight: 600;";
private const string StyleBtnNeutral = "background-color: #1e2535; color: #8899bb; border: 1px solid #2a3550; border-radius: 3px; padding: 8px 14px; font-size: 12px; font-weight: 600;";
#endregion
#region Constructor
public ApexWorldEditorWindow( Widget parent ) : base( parent )
{
WindowTitle = "Apex World";
MinimumSize = new Vector2( 400, 500 );
Layout = Layout.Column();
Layout.Spacing = 0;
BuildHeaderAndTabs();
BuildTabConfigs();
BuildTabTerrains();
BuildTabSpawners();
BuildFooter();
Refresh();
ShowTab( 0 );
}
#endregion
#region Header & Tabs
private void BuildHeaderAndTabs()
{
var header = new Widget( this );
header.Layout = Layout.Column();
header.Layout.Spacing = 0;
header.Layout.Margin = 0;
header.SetStyles( "background-color: #141820; border-bottom: 1px solid #222a38;" );
// Title and status row
var titleRow = new Widget( header );
titleRow.Layout = Layout.Row();
titleRow.Layout.Spacing = 8;
titleRow.Layout.Margin = new Margin( 12, 10, 12, 10 );
var title = new Label( "APEX WORLD", titleRow );
title.SetStyles( "font-size: 18px; font-weight: bold; color: #e8d5a0; letter-spacing: 4px;" );
_statusLabel = new Label( "", titleRow );
_statusLabel.SetStyles( "font-size: 12px; color: #556; letter-spacing: 1px; margin-left: 8px;" );
titleRow.Layout.Add( title );
titleRow.Layout.Add( _statusLabel );
titleRow.Layout.AddStretchCell();
var refreshBtn = new IconButton( "refresh" );
refreshBtn.ToolTip = "Refresh";
refreshBtn.OnClick += Refresh;
titleRow.Layout.Add( refreshBtn );
header.Layout.Add( titleRow );
// Tab bar - spread evenly
var tabBar = new Widget( header );
tabBar.Layout = Layout.Row();
tabBar.Layout.Spacing = 0;
tabBar.Layout.Margin = 0;
tabBar.SetStyles( "background-color: #0f1218; border-top: 1px solid #222a38;" );
_btnConfigs = new Button( "Configs", tabBar ); _btnConfigs.SetStyles( StyleTabInactive ); _btnConfigs.Clicked += () => ShowTab( 0 );
_btnTerrains = new Button( "Terrains", tabBar ); _btnTerrains.SetStyles( StyleTabInactive ); _btnTerrains.Clicked += () => ShowTab( 1 );
_btnSpawners = new Button( "Spawners", tabBar ); _btnSpawners.SetStyles( StyleTabInactive ); _btnSpawners.Clicked += () => ShowTab( 2 );
tabBar.Layout.Add( _btnConfigs, 1 );
tabBar.Layout.Add( _btnTerrains, 1 );
tabBar.Layout.Add( _btnSpawners, 1 );
header.Layout.Add( tabBar );
Layout.Add( header );
}
#endregion
#region Tab Management
private void ShowTab( int index )
{
_tabConfigs.Visible = index == 0;
_tabTerrains.Visible = index == 1;
_tabSpawners.Visible = index == 2;
_btnConfigs.SetStyles( index == 0 ? StyleTabActive : StyleTabInactive );
_btnTerrains.SetStyles( index == 1 ? StyleTabActive : StyleTabInactive );
_btnSpawners.SetStyles( index == 2 ? StyleTabActive : StyleTabInactive );
if ( index == 0 ) RebuildConfigList();
if ( index == 1 ) RebuildTerrainList();
if ( index == 2 ) RebuildSpawnerList();
}
#endregion
#region Tab — Configs
private void BuildTabConfigs()
{
_tabConfigs = new Widget( this );
_tabConfigs.Layout = Layout.Column();
_tabConfigs.Layout.Spacing = 0;
_tabConfigs.Visible = false;
SectionLabel( _tabConfigs, "MANAGER" );
_createManagerBtnContainer = new Widget( _tabConfigs );
_createManagerBtnContainer.Layout = Layout.Column();
_createManagerBtnContainer.Layout.Spacing = 6;
_createManagerBtnContainer.Layout.Margin = 12;
_tabConfigs.Layout.Add( _createManagerBtnContainer );
SectionLabel( _tabConfigs, "PROJECT SETTINGS" );
var scroll = new ScrollArea( _tabConfigs );
scroll.MinimumHeight = 200;
_configListContainer = new Widget( scroll );
_configListContainer.Layout = Layout.Column();
_configListContainer.Layout.Spacing = 1;
scroll.Canvas = _configListContainer;
_tabConfigs.Layout.Add( scroll );
_tabConfigs.Layout.AddStretchCell();
Layout.Add( _tabConfigs );
}
private void RebuildConfigList()
{
_createManagerBtnContainer.DestroyChildren();
if ( _manager == null )
{
var createManagerBtn = new Button( "Create Manager in Scene", "add_circle", _createManagerBtnContainer );
createManagerBtn.SetStyles( StyleBtnNeutral );
createManagerBtn.Clicked += () =>
{
var scene = SceneEditorSession.Active?.Scene;
if ( scene == null ) return;
var go = scene.CreateObject();
go.Name = "ApexWorldManager";
go.Components.Create<ApexWorldManager>();
Refresh();
};
_createManagerBtnContainer.Layout.Add( createManagerBtn );
}
_configListContainer.DestroyChildren();
// List configuration files from ProjectSettings
var projectSettingsPath = System.IO.Path.Combine(
System.IO.Path.GetDirectoryName( System.Reflection.Assembly.GetExecutingAssembly().Location ) ?? "",
"..", "..", "..", "ProjectSettings" );
try
{
if ( System.IO.Directory.Exists( projectSettingsPath ) )
{
var configFiles = System.IO.Directory.GetFiles( projectSettingsPath, "*.config" );
if ( configFiles.Length == 0 )
{
EmptyLabel( _configListContainer, "No configuration files found" );
return;
}
foreach ( var filePath in configFiles )
{
var fileName = System.IO.Path.GetFileName( filePath );
var row = new Widget( _configListContainer );
row.Layout = Layout.Row();
row.Layout.Spacing = 8;
row.Layout.Margin = new Margin( 12, 6, 8, 6 );
row.SetStyles( StyleRow );
row.MinimumHeight = 50;
var col = new Widget( row );
col.Layout = Layout.Column();
col.Layout.Spacing = 2;
var nameLabel = new Label( fileName, col );
nameLabel.SetStyles( "font-size: 12px; color: #ccc; font-weight: bold;" );
var infoLabel = new Label( $"Path: {filePath.Replace( "\\", "/" )}", col );
infoLabel.SetStyles( "font-size: 9px; color: #556;" );
col.Layout.Add( nameLabel );
col.Layout.Add( infoLabel );
row.Layout.Add( col, 1 );
var openBtn = new IconButton( "open_in_new" );
openBtn.ToolTip = "Open in editor";
openBtn.OnClick += () =>
{
Log.Info( $"[ApexWorld] Opening config: {fileName}" );
};
row.Layout.Add( openBtn );
_configListContainer.Layout.Add( row );
}
}
else
{
EmptyLabel( _configListContainer, "ProjectSettings folder not found" );
}
}
catch ( Exception e )
{
Log.Warning( $"[ApexWorld] Error reading config files: {e.Message}" );
EmptyLabel( _configListContainer, "Error reading configuration files" );
}
}
#endregion
#region Tab — Terrains
private void BuildTabTerrains()
{
_tabTerrains = new Widget( this );
_tabTerrains.Layout = Layout.Column();
_tabTerrains.Layout.Spacing = 0;
_tabTerrains.Visible = false;
SectionLabel( _tabTerrains, "TERRAINS IN SCENE" );
var scroll = new ScrollArea( _tabTerrains );
scroll.MinimumHeight = 300;
_terrainListContainer = new Widget( scroll );
_terrainListContainer.Layout = Layout.Column();
_terrainListContainer.Layout.Spacing = 1;
scroll.Canvas = _terrainListContainer;
_tabTerrains.Layout.Add( scroll );
_tabTerrains.Layout.AddStretchCell();
Layout.Add( _tabTerrains );
}
private void RebuildTerrainList()
{
_terrainListContainer.DestroyChildren();
if ( _manager == null ) { EmptyLabel( _terrainListContainer, "No manager in scene" ); return; }
var terrains = _manager.GetWorldCache()?.GetAllTerrains();
if ( terrains == null || terrains.Count == 0 ) { EmptyLabel( _terrainListContainer, "No terrains found" ); return; }
foreach ( var terrain in terrains )
{
if ( terrain == null ) continue;
var row = new Widget( _terrainListContainer );
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.Layout.Margin = new Margin( 12, 6, 8, 6 );
row.SetStyles( StyleRow );
row.MinimumHeight = 50;
var col = new Widget( row );
col.Layout = Layout.Column();
col.Layout.Spacing = 2;
var name = new Label( terrain.GameObject.Name, col );
name.SetStyles( "font-size: 12px; color: #ccc; font-weight: bold;" );
var info = new Label( $"Res: {terrain.Storage?.Resolution} | Size: {terrain.Storage?.TerrainSize:F0}", col );
info.SetStyles( "font-size: 10px; color: #556;" );
col.Layout.Add( name );
col.Layout.Add( info );
row.Layout.Add( col, 1 );
var resetBtn = new IconButton( "layers_clear" );
resetBtn.ToolTip = "Reset textures";
resetBtn.OnClick += () =>
{
if ( terrain?.Storage == null ) return;
var mat = new CompactTerrainMaterial { BaseTextureId = 0, OverlayTextureId = 0, BlendFactor = 0, IsHole = false };
for ( int i = 0; i < terrain.Storage.ControlMap.Length; i++ )
terrain.Storage.ControlMap[i] = mat.Packed;
terrain.SyncGPUTexture();
Log.Info( $"[ApexWorld] Reset {terrain.GameObject.Name}" );
};
var selectBtn = new IconButton( "my_location" );
selectBtn.ToolTip = "Select in scene";
selectBtn.OnClick += () => SceneEditorSession.Active?.Selection.Set( terrain.GameObject );
row.Layout.Add( resetBtn );
row.Layout.Add( selectBtn );
_terrainListContainer.Layout.Add( row );
}
}
#endregion
#region Tab — Spawners
private void BuildTabSpawners()
{
_tabSpawners = new Widget( this );
_tabSpawners.Layout = Layout.Column();
_tabSpawners.Layout.Spacing = 0;
_tabSpawners.Visible = false;
SectionLabel( _tabSpawners, "SPAWNERS IN SCENE" );
var scroll = new ScrollArea( _tabSpawners );
scroll.MinimumHeight = 260;
_spawnerListContainer = new Widget( scroll );
_spawnerListContainer.Layout = Layout.Column();
_spawnerListContainer.Layout.Spacing = 0;
scroll.Canvas = _spawnerListContainer;
_tabSpawners.Layout.Add( scroll );
SectionLabel( _tabSpawners, "CREATE SPAWNER" );
var createRow = new Widget( _tabSpawners );
createRow.Layout = Layout.Row();
createRow.Layout.Spacing = 8;
createRow.Layout.Margin = new Margin( 12, 10, 12, 10 );
createRow.SetStyles( "background-color: #141820;" );
var combo = new ComboBox( createRow );
combo.AddItem( "TextureSpawner", null );
combo.AddItem( "DetailSpawner", null );
combo.ItemChanged += () =>
{
if ( _comboInitialized )
{
_newSpawnerType = (SpawnerCreateType)combo.CurrentIndex;
Log.Info( (SpawnerCreateType)combo.CurrentIndex );
}
};
_comboInitialized = true;
createRow.Layout.Add( combo );
var addBtn = new Button( "Add Spawner", "add", createRow );
addBtn.SetStyles( StyleBtnGreen );
addBtn.Clicked += CreateSpawner;
createRow.Layout.Add( addBtn );
_tabSpawners.Layout.Add( createRow );
Layout.Add( _tabSpawners );
}
private void RebuildSpawnerList()
{
_spawnerListContainer.DestroyChildren();
if ( _manager?.Spawners == null || _manager.Spawners.Length == 0 )
{
EmptyLabel( _spawnerListContainer, "No spawners assigned" );
return;
}
var spawners = _manager.Spawners;
for ( int i = 0; i < spawners.Length; i++ )
{
var spawner = spawners[i];
if ( spawner == null ) continue;
int index = i;
var row = new Widget( _spawnerListContainer );
row.Layout = Layout.Row();
row.Layout.Spacing = 6;
row.Layout.Margin = new Margin( 12, 6, 8, 6 );
row.SetStyles( StyleRow );
row.MinimumHeight = 50;
var col = new Widget( row );
col.Layout = Layout.Column();
col.Layout.Spacing = 2;
var nameLabel = new Label( spawner.SpawnerName ?? spawner.GetType().Name, col );
nameLabel.SetStyles( "font-size: 12px; color: #ccc; font-weight: bold;" );
var typeLabel = new Label( $"{spawner.GetType().Name} | Range: {spawner.Range:F0}", col );
typeLabel.SetStyles( "font-size: 10px; color: #556;" );
col.Layout.Add( nameLabel );
col.Layout.Add( typeLabel );
row.Layout.Add( col, 1 );
var upBtn = new IconButton( "arrow_upward" );
upBtn.ToolTip = "Move up";
upBtn.Enabled = index > 0;
upBtn.OnClick += () => MoveSpawner( index, -1 );
var downBtn = new IconButton( "arrow_downward" );
downBtn.ToolTip = "Move down";
downBtn.Enabled = index < spawners.Length - 1;
downBtn.OnClick += () => MoveSpawner( index, 1 );
var generateBtn = new IconButton( "play_arrow" );
generateBtn.ToolTip = "Generate";
generateBtn.SetStyles( "color: #70d090;" );
generateBtn.OnClick += () =>
{
spawner.OnBeforeGenerate();
spawner.Generate();
spawner.OnGenerationFinished();
Log.Info( $"[ApexWorld] Generated: {spawner}" );
};
var selectBtn = new IconButton( "my_location" );
selectBtn.ToolTip = "Select in scene";
selectBtn.OnClick += () => SceneEditorSession.Active?.Selection.Set( spawner.GameObject );
var destroyBtn = new IconButton( "delete" );
destroyBtn.ToolTip = "Destroy spawner";
destroyBtn.SetStyles( "color: #d07070;" );
destroyBtn.OnClick += () => DestroySpawner( spawner );
row.Layout.Add( upBtn );
row.Layout.Add( downBtn );
row.Layout.Add( generateBtn );
row.Layout.Add( selectBtn );
row.Layout.Add( destroyBtn );
_spawnerListContainer.Layout.Add( row );
}
}
private void MoveSpawner( int index, int direction )
{
var list = _manager.Spawners.ToList();
int newIdx = index + direction;
if ( newIdx < 0 || newIdx >= list.Count ) return;
(list[index], list[newIdx]) = (list[newIdx], list[index]);
_manager.Spawners = list.ToArray();
RebuildSpawnerList();
}
private void CreateSpawner()
{
if ( _manager == null ) return;
// Create GameObject in the same scene as the manager
var scene = _manager.GameObject.Scene;
if ( scene == null ) return;
var go = scene.CreateObject( false );
go.Name = _newSpawnerType.ToString();
go.Parent = _manager.GameObject;
BaseSpawner spawner = _newSpawnerType switch
{
SpawnerCreateType.TextureSpawner => go.Components.Create<TextureSpawner>(),
SpawnerCreateType.DetailSpawner => go.Components.Create<DetailsSpawner>(),
_ => null
};
if ( spawner == null ) { go.Destroy(); return; }
var list = (_manager.Spawners ?? Array.Empty<BaseSpawner>()).ToList();
list.Add( spawner );
_manager.Spawners = list.ToArray();
Log.Info( $"[ApexWorld] Created {_newSpawnerType}" );
RebuildSpawnerList();
}
private void DestroySpawner( BaseSpawner spawner )
{
var list = _manager.Spawners.ToList();
list.Remove( spawner );
_manager.Spawners = list.ToArray();
spawner.GameObject.Destroy();
RebuildSpawnerList();
}
#endregion
#region Footer
private void BuildFooter()
{
var footer = new Widget( this );
footer.Layout = Layout.Row();
footer.Layout.Spacing = 8;
footer.Layout.Margin = new Margin( 12, 10, 12, 10 );
footer.SetStyles( "background-color: #141820; border-top: 1px solid #222a38;" );
var generateAll = new Button( "Generate All", "play_arrow", footer );
generateAll.SetStyles( StyleBtnGreen );
generateAll.Clicked += OnGenerateAll;
var resetAll = new Button( "Reset Textures", "layers_clear", footer );
resetAll.SetStyles( StyleBtnRed );
resetAll.Clicked += OnResetAll;
footer.Layout.Add( generateAll );
footer.Layout.Add( resetAll );
footer.Layout.AddStretchCell();
Layout.Add( footer );
}
private void OnGenerateAll()
{
if ( _manager?.Spawners == null ) return;
foreach ( var spawner in _manager.Spawners )
{
if ( spawner == null ) continue;
spawner.OnBeforeGenerate();
spawner.Generate();
spawner.OnGenerationFinished();
}
Log.Info( "[ApexWorld] Generated all spawners" );
}
private void OnResetAll() => _manager?.ResetAllTerrainsTextures();
#endregion
#region Manager & Refresh
private ApexWorldManager FindManager() =>
SceneEditorSession.Active?.Scene?
.GetAllComponents<ApexWorldManager>()
.FirstOrDefault();
private void Refresh()
{
_manager = FindManager();
if ( _manager != null )
{
_statusLabel.Text = $"Manager: {_manager.GameObject.Name}";
_statusLabel.SetStyles( "font-size: 11px; color: #70c080; letter-spacing: 1px;" );
_manager.RegisterSpawners();
}
else
{
_statusLabel.Text = "No manager found";
_statusLabel.SetStyles( "font-size: 11px; color: #c07070; letter-spacing: 1px;" );
}
RebuildConfigList();
RebuildSpawnerList();
RebuildTerrainList();
}
#endregion
#region Helpers
private static void SectionLabel( Widget parent, string text )
{
var row = new Widget( parent );
row.Layout = Layout.Row();
row.Layout.Margin = new Margin( 12, 6, 12, 6 );
row.SetStyles( "background-color: #161b26; border-bottom: 1px solid #222a38; border-top: 1px solid #222a38;" );
var label = new Label( text, row );
label.SetStyles( "font-size: 10px; font-weight: bold; color: #445; letter-spacing: 3px;" );
parent.Layout.Add( row );
}
private static void EmptyLabel( Widget parent, string text )
{
var label = new Label( text, parent );
label.SetStyles( "color: #445; font-size: 11px; padding: 20px; text-align: center;" );
parent.Layout.Add( label );
}
#endregion
}