6578 results

global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Debug Assertions" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "debugassertions" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "quality" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "quality.debugassertions" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2/10/2026 8:52:37 AM" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "24" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.117.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.117.0")]
using System;

namespace Editor;

/// <summary>
/// Blender-style radial menu with button boxes arranged in a circle.
/// Opens on Ctrl+Right Click.
/// </summary>
public class PieMenu : Widget
{
	public class PieOption
	{
		public string Text { get; set; }
		public string Icon { get; set; }
		public Action Action { get; set; }
		public bool Enabled { get; set; } = true;
	}

	private List<PieOption> _options = new();
	private int _hoveredIndex = -1;
	private Vector2 _centerPosition;
	private float _currentIndicatorAngle = -90f; // Start at top
	private float _targetIndicatorAngle = -90f;

	public float Radius { get; set; } = 180f;
	public float CenterRadius { get; set; } = 30f;
	public float CenterRingThickness { get; set; } = 10f;
	public float ButtonPadding { get; set; } = 20f;
	public float ButtonHeight { get; set; } = 28f;

	// s&box themed colors
	public Color ButtonColor { get; set; } = Theme.ButtonBackground;
	public Color ButtonHoverColor { get; set; } = Theme.Blue;
	public Color CenterRingColor { get; set; } = Theme.ControlBackground.Darken( 0.2f );
	public Color IndicatorColor { get; set; } = Theme.Yellow;

	// Track which button this menu should respond to
	public MouseButtons TriggerButton { get; set; } = MouseButtons.Forward;

	public PieMenu( Widget parent = null ) : base( parent )
	{
		WindowFlags = WindowFlags.Popup | WindowFlags.FramelessWindowHint | WindowFlags.NoDropShadowWindowHint;
		TranslucentBackground = true;
	}

	public PieOption AddOption( string text, string icon = null, Action action = null )
	{
		var option = new PieOption
		{
			Text = text,
			Icon = icon,
			Action = action
		};

		_options.Add( option );
		return option;
	}

	public void Clear()
	{
		_options.Clear();
		_hoveredIndex = -1;
	}

	public void OpenAtCursor()
	{
		OpenAt( Application.CursorPosition );
	}

	public void OpenAt( Vector2 position )
	{
		if ( _options.Count == 0 ) return;

		Paint.SetDefaultFont( 10, 500 );
		float maxButtonWidth = 0;
		foreach ( var option in _options )
		{
			var textSize = Paint.MeasureText( option.Text );
			float buttonWidth = textSize.x + ButtonPadding * 2;
			maxButtonWidth = Math.Max( maxButtonWidth, buttonWidth );
		}

		var size = (Radius + maxButtonWidth + 40) * 2;
		var widgetSize = new Vector2( size );

		Size = widgetSize;
		MinimumSize = widgetSize;
		MaximumSize = widgetSize;
		Position = position - (widgetSize / 2f);

		_centerPosition = widgetSize / 2;
		_hoveredIndex = -1;

		Show();
		Focus();
		MouseTracking = true;

		var localPos = Application.CursorPosition;
		UpdateHoveredOption( localPos );
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		if ( _options.Count == 0 ) return;

		Paint.Antialiasing = true;

		var center = LocalRect.Center;
		int optionCount = _options.Count;
		float angleStep = 360f / optionCount;
		float startAngle = -90f;

		// Draw button boxes positioned around the circle
		for ( int i = 0; i < optionCount; i++ )
		{
			var option = _options[i];

			float angle = startAngle + (i * angleStep);
			float angleRad = angle * MathF.PI / 180f;

			// Calculate button position
			var buttonCenter = center + new Vector2(
				MathF.Cos( angleRad ) * Radius,
				MathF.Sin( angleRad ) * Radius
			);

			// Measure text width to size button
			Paint.SetDefaultFont( 10, 500 );
			var textSize = Paint.MeasureText( option.Text );

			// Button width = text width + padding
			float buttonWidth = textSize.x + ButtonPadding * 2;

			var buttonRect = new Rect(
				buttonCenter.x - buttonWidth / 2,
				buttonCenter.y - ButtonHeight / 2,
				buttonWidth,
				ButtonHeight
			);

			var buttonColor = i == _hoveredIndex ? ButtonHoverColor : ButtonColor;
			Paint.ClearPen();
			Paint.SetBrush( buttonColor );
			Paint.DrawRect( buttonRect, 3 );

			// Draw text - centered
			Paint.SetPen( Color.White );
			Paint.SetDefaultFont( 10, 500 );

			var textRect = buttonRect.Shrink( 10, 0 );
			Paint.DrawText( textRect, option.Text, TextFlag.Center );
		}

		// Draw center ring (donut shape)
		Paint.ClearPen();
		Paint.SetBrush( CenterRingColor );
		DrawRing( center, CenterRadius - CenterRingThickness, CenterRadius );

		if ( _hoveredIndex >= 0 && _hoveredIndex < _options.Count )
		{
			// Much faster interpolation for snappier feel
			float angleDiff = _targetIndicatorAngle - _currentIndicatorAngle;

			// Handle wraparound (shortest path)
			if ( angleDiff > 180f ) angleDiff -= 360f;
			if ( angleDiff < -180f ) angleDiff += 360f;

			_currentIndicatorAngle += angleDiff * 0.6f; // Increased from 0.3f for faster response

			// Normalize angle
			if ( _currentIndicatorAngle < 0 ) _currentIndicatorAngle += 360f;
			if ( _currentIndicatorAngle >= 360f ) _currentIndicatorAngle -= 360f;

			// Draw a colored arc segment on the ring
			float arcSpan = 40f;
			Paint.SetBrush( IndicatorColor );
			DrawArcSegment( center, CenterRadius - CenterRingThickness, CenterRadius, _currentIndicatorAngle - arcSpan / 2, _currentIndicatorAngle + arcSpan / 2 );
		}
	}

	/// <summary>
	/// Draw a ring (donut shape)
	/// </summary>
	private void DrawRing( Vector2 center, float innerRadius, float outerRadius )
	{
		const int segments = 64;
		var points = new List<Vector2>();

		// Outer circle
		for ( int i = 0; i <= segments; i++ )
		{
			float angle = (i / (float)segments) * 360f;
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * outerRadius );
		}

		// Inner circle (reverse)
		for ( int i = segments; i >= 0; i-- )
		{
			float angle = (i / (float)segments) * 360f;
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * innerRadius );
		}

		Paint.DrawPolygon( points.ToArray() );
	}

	/// <summary>
	/// Draw an arc segment on a ring
	/// </summary>
	private void DrawArcSegment( Vector2 center, float innerRadius, float outerRadius, float startAngle, float endAngle )
	{
		const int segments = 20;
		float angleRange = endAngle - startAngle;
		int segmentCount = Math.Max( 2, (int)(segments * angleRange / 360f) );

		var points = new List<Vector2>();

		// Inner arc
		for ( int i = 0; i <= segmentCount; i++ )
		{
			float angle = startAngle + (angleRange * i / segmentCount);
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * innerRadius );
		}

		// Outer arc (reverse)
		for ( int i = segmentCount; i >= 0; i-- )
		{
			float angle = startAngle + (angleRange * i / segmentCount);
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * outerRadius );
		}

		Paint.DrawPolygon( points.ToArray() );
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		UpdateHoveredOption( e.LocalPosition );
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );
		e.Accepted = true;
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		// Check if this is the button that opened this menu
		if ( e.Button == TriggerButton )
		{
			ExecuteHoveredOptionAndClose();
			e.Accepted = true;
			return;
		}

		base.OnMouseReleased( e );
	}

	/// <summary>
	/// Execute the currently hovered option and close the menu
	/// </summary>
	public void ExecuteHoveredOptionAndClose()
	{
		if ( _hoveredIndex >= 0 && _hoveredIndex < _options.Count )
		{
			var option = _options[_hoveredIndex];
			if ( option.Enabled )
			{
				option.Action?.Invoke();
			}
		}

		Close();
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		base.OnKeyPress( e );

		if ( e.Key == KeyCode.Escape )
		{
			Close();
			e.Accepted = true;
		}
	}

	private void UpdateHoveredOption( Vector2 mousePos )
	{
		var center = LocalRect.Center;
		var offset = mousePos - center;
		float distance = offset.Length;

		// More forgiving selection area
		if ( distance < 10f || distance > Radius + 250 )
		{
			if ( _hoveredIndex != -1 )
			{
				_hoveredIndex = -1;
				Update();
			}
			return;
		}

		// Calculate angle from center to mouse
		float mouseAngle = MathF.Atan2( offset.y, offset.x ) * 180f / MathF.PI;

		// Find the closest button by angular distance
		int optionCount = _options.Count;
		float angleStep = 360f / optionCount;
		float startAngle = -90f;

		int closestIndex = -1;
		float closestDistance = float.MaxValue;

		for ( int i = 0; i < optionCount; i++ )
		{
			// Calculate the angle of this button's center
			float buttonAngle = startAngle + (i * angleStep);

			// Normalize both angles to 0-360
			float normMouseAngle = mouseAngle;
			if ( normMouseAngle < 0 ) normMouseAngle += 360f;
			float normButtonAngle = buttonAngle;
			if ( normButtonAngle < 0 ) normButtonAngle += 360f;

			// Calculate angular distance (shortest path around the circle)
			float angularDist = Math.Abs( normMouseAngle - normButtonAngle );
			if ( angularDist > 180f ) angularDist = 360f - angularDist;

			if ( angularDist < closestDistance )
			{
				closestDistance = angularDist;
				closestIndex = i;
			}
		}

		// Set target angle to the selected button's angle for smooth snapping
		if ( closestIndex != -1 )
		{
			_targetIndicatorAngle = startAngle + (closestIndex * angleStep);
		}

		_hoveredIndex = closestIndex;
		Update();
	}

	[Shortcut( "editor.paste.color", "CTRL+V", typeof( SceneViewWidget ) )]
	public static void PasteFromClipboard()
	{
		var clipboard = EditorUtility.Clipboard.Paste();

		if ( string.IsNullOrWhiteSpace( clipboard ) )
		{
			// Try GameObject paste if clipboard text is empty
			PasteGameObject();
			return;
		}

		// Try to parse as hex color first
		if ( clipboard.StartsWith( "#" ) )
		{
			if ( TryParseHexColor( clipboard, out Color color ) )
			{
				PasteColor( color );
				return;
			}
			else
			{
				Log.Warning( $"Failed to parse color from clipboard: {clipboard}" );
				return;
			}
		}

		// Try to handle GameObject paste
		PasteGameObject();
	}

	private static void PasteColor( Color color )
	{
		using var scope = SceneEditorSession.Scope();

		var selection = SceneEditorSession.Active.Selection;

		// Get all MeshComponents and ModelRenderers from selected GameObjects
		var meshComponents = selection.OfType<GameObject>()
			.Select( x => x.GetComponent<MeshComponent>() )
			.Where( x => x.IsValid() )
			.ToList();

		var modelRenderers = selection.OfType<GameObject>()
			.Select( x => x.GetComponent<ModelRenderer>() )
			.Where( x => x.IsValid() )
			.ToList();

		var totalCount = meshComponents.Count + modelRenderers.Count;

		if ( totalCount == 0 )
		{
			Log.Info( "No mesh or model renderer components selected" );
			return;
		}

		// Combine both lists for undo tracking
		var allComponents = meshComponents.Cast<Component>()
			.Concat( modelRenderers.Cast<Component>() )
			.ToList();

		using ( SceneEditorSession.Active.UndoScope( "Paste Color" )
			.WithComponentChanges( allComponents )
			.Push() )
		{
			// Apply color to MeshComponents
			foreach ( var component in meshComponents )
			{
				component.Color = color;
			}

			// Apply color to ModelRenderers
			foreach ( var component in modelRenderers )
			{
				component.Tint = color;
			}
		}

		Log.Info( $"Applied color to {meshComponents.Count} mesh component(s) and {modelRenderers.Count} model renderer(s)" );
	}

	private static void PasteGameObject()
	{
		var session = SceneEditorSession.Active;
		if ( session == null ) return;

		// Get the active scene viewport
		var sceneView = SceneViewWidget.Current;
		if ( sceneView?.LastSelectedViewportWidget == null ) return;

		var viewport = sceneView.LastSelectedViewportWidget;

		// First, paste the standard way
		EditorScene.Paste();

		// Get the pasted objects
		var pastedObjects = session.Selection.OfType<GameObject>().ToList();
		if ( pastedObjects.Count == 0 ) return;

		// Compute the average point of all pasted objects
		Vector3 middlePoint = Vector3.Zero;
		foreach ( var go in pastedObjects )
			middlePoint += go.WorldPosition;

		middlePoint /= pastedObjects.Count;

		// Get mouse position in viewport and trace
		var mousePosition = SceneViewportWidget.MousePosition;
		var camera = viewport.Renderer.Camera;

		if ( !camera.IsValid() ) return;

		// Create ray from mouse position
		var ray = camera.ScreenPixelToRay( mousePosition );

		// Trace to find world position
		var trace = session.Scene.Trace
			.Ray( ray, 10000f )
			.UseRenderMeshes( true )
			.UsePhysicsWorld( false )
			.Run();

		if ( trace.Hit )
		{
			using ( session.UndoScope( "Paste at Mouse" ).Push() )
			{
				// Reposition all game objects relative to new center
				foreach ( var go in pastedObjects )
				{
					Vector3 offset = go.WorldPosition - middlePoint;
					go.WorldPosition = trace.HitPosition + offset;
				}

				Log.Info( $"Pasted {pastedObjects.Count} GameObject(s) at mouse position" );
			}
		}
	}

	static bool TryParseHexColor( string hex, out Color color )
	{
		color = default;

		if ( string.IsNullOrEmpty( hex ) )
			return false;

		hex = hex.TrimStart( '#' );

		if ( hex.Length != 6 && hex.Length != 8 )
			return false;

		try
		{
			int r = Convert.ToInt32( hex.Substring( 0, 2 ), 16 );
			int g = Convert.ToInt32( hex.Substring( 2, 2 ), 16 );
			int b = Convert.ToInt32( hex.Substring( 4, 2 ), 16 );
			int a = hex.Length == 8 ? Convert.ToInt32( hex.Substring( 6, 2 ), 16 ) : 255;

			color = new Color( r / 255f, g / 255f, b / 255f, a / 255f );
			return true;
		}
		catch
		{
			return false;
		}
	}

	public IReadOnlyList<PieOption> Options => _options.AsReadOnly();
}
using Editor;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static Editor.Label;

internal sealed class MappingToolSettingsWindow : BaseWindow
{
	private NavigationView Navigation { get; }

	[Menu( "Editor", "Mapping Tools/Settings", Icon = "settings" )]
	public static MappingToolSettingsWindow Open()
	{
		var window = new MappingToolSettingsWindow();
		window.Show();
		return window;
	}

	public MappingToolSettingsWindow()
	{
		SetModal( true, true );
		Size = new Vector2( 640, 420 );
		MinimumSize = Size;
		TranslucentBackground = true;
		NoSystemBackground = true;

		WindowTitle = "Mapping Tool Settings";
		SetWindowIcon( "settings" );

		Layout = Layout.Column();
		Layout.Margin = 4;
		Layout.Spacing = 4;

		Navigation = new NavigationView();
		Layout.Add( Navigation );

		BuildPages();
	}

	private void BuildPages()
	{
		Navigation.AddSectionHeader( "Mapping Tools" );

		Navigation.AddPage( "General", "tune", new PageGeneral( this ) );
		Navigation.AddPage( "Materials Gallery", "image", new PageMaterialBrowser( this ) );
		Navigation.AddPage( "Render Pie Menu", "menu", new PageRenderPieMenu( this ) );
		Navigation.AddPage( "Mapping Pie Menu", "edit", new PageMappingPieMenu( this ) );
		Navigation.AddPage( "Auto-Save", "save", new PageAutoSave( this ) );
	}
}

internal sealed class PageGeneral : Widget
{
	public PageGeneral( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 32;
		Layout.Spacing = 16;

		Layout.Add( new Subtitle( "General Settings" ) );
		Layout.Add( new Editor.Label( "General mapping workflow enhancements" ) { WordWrap = true } );

		var sheet = new Editor.ControlSheet();

		sheet.AddProperty( () => MappingToolSettings.DoubleClickToMeshMode );

		Layout.Add( sheet );

		Layout.AddStretchCell();
	}
}

internal sealed class PageMaterialBrowser : Widget
{
	private Layout _orgListLayout;

	public PageMaterialBrowser( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 32;
		Layout.Spacing = 16;

		Layout.Add( new Subtitle( "Materials Gallery" ) );

		var sheet = new Editor.ControlSheet();

		// Add default org property
		sheet.AddProperty( () => MappingToolSettings.DefaultOrganization );

		Layout.Add( sheet );

		// Organizations list section
		Layout.Add( new Editor.Label.Title( "Additional Organizations" ) );
		Layout.Add( new Editor.Label( "Add organizations to search for materials" ) { WordWrap = true } );

		// Scrollable container for organizations
		var scrollArea = new ScrollArea( this );
		scrollArea.MinimumHeight = 150;
		scrollArea.MaximumHeight = 200;
		Layout.Add( scrollArea );

		var container = new Widget( scrollArea );
		_orgListLayout = container.Layout = Layout.Column();
		_orgListLayout.Spacing = 4;
		scrollArea.Canvas = container;

		// Add organization controls
		var addRow = Layout.AddRow();
		addRow.Spacing = 8;

		var orgInput = new LineEdit();
		orgInput.PlaceholderText = "Enter organization name...";
		orgInput.MinimumWidth = 200;
		addRow.Add( orgInput, 1 );

		var addButton = new Editor.Button( "Add", "add" );
		addButton.Clicked += () =>
		{
			var org = orgInput.Text?.Trim();
			if ( !string.IsNullOrEmpty( org ) )
			{
				MappingToolSettings.AddOrganization( org );
				orgInput.Text = "";
				RefreshOrgList();
			}
		};
		addRow.Add( addButton );

		var resetButton = new Editor.Button( "Reset to Defaults" );
		resetButton.Clicked += () =>
		{
			MappingToolSettings.ResetMaterialGalleryDefaults();
			RefreshOrgList();
		};
		Layout.Add( resetButton );

		Layout.AddStretchCell();

		RefreshOrgList();
	}

	private void RefreshOrgList()
	{
		// Clear existing items
		_orgListLayout.Clear( true );

		var orgs = MappingToolSettings.AdditionalOrganizations.ToList();

		if ( orgs.Count == 0 )
		{
			var emptyLabel = new Editor.Label( "No additional organizations added" );
			emptyLabel.SetStyles( "color: rgba(255,255,255,0.4); font-style: italic;" );
			_orgListLayout.Add( emptyLabel );
			return;
		}

		foreach ( var org in orgs )
		{
			var row = _orgListLayout.AddRow();
			row.Spacing = 8;

			// Org name label
			var nameLabel = new Editor.Label( org );
			nameLabel.MinimumWidth = 150;
			row.Add( nameLabel, 1 );

			// Remove button
			var removeBtn = new Editor.Button.Primary( "", "close" );
			removeBtn.MinimumWidth = 32;
			removeBtn.MaximumWidth = 32;
			removeBtn.ToolTip = $"Remove {org}";
			removeBtn.Clicked += () =>
			{
				MappingToolSettings.RemoveOrganization( org );
				RefreshOrgList();
			};
			row.Add( removeBtn );
		}
	}
}

internal sealed class PageRenderPieMenu : Widget
{
	public PageRenderPieMenu( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 32;
		Layout.Spacing = 16;

		Layout.Add( new Subtitle( "Render Pie Menu" ) );
		Layout.Add( new Editor.Label( "Quick access to render modes and viewport settings" ) { WordWrap = true } );

		var sheet = new Editor.ControlSheet();

		// Automatically generate UI from the settings properties
		sheet.AddProperty( () => MappingToolSettings.PieMenuButton );
		sheet.AddProperty( () => MappingToolSettings.PieMenuUseModifier );
		sheet.AddProperty( () => MappingToolSettings.PieMenuModifierKey );
		sheet.AddProperty( () => MappingToolSettings.PieMenuSize );

		var resetButton = new Editor.Button( "Reset to Defaults" );
		resetButton.Clicked += () =>
		{
			MappingToolSettings.ResetPieMenuDefaults();
		};

		Layout.Add( sheet );
		Layout.Add( resetButton );
		Layout.AddStretchCell();
	}
}

internal sealed class PageMappingPieMenu : Widget
{
	public PageMappingPieMenu( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 32;
		Layout.Spacing = 16;

		Layout.Add( new Subtitle( "Mapping Pie Menu" ) );
		Layout.Add( new Editor.Label( "Quick access to mesh editing modes (Object, Vertex, Edge, Face)" ) { WordWrap = true } );

		var sheet = new Editor.ControlSheet();

		// Automatically generate UI from the settings properties
		sheet.AddProperty( () => MappingToolSettings.MappingPieMenuButton );
		sheet.AddProperty( () => MappingToolSettings.MappingPieMenuUseModifier );
		sheet.AddProperty( () => MappingToolSettings.MappingPieMenuModifierKey );
		sheet.AddProperty( () => MappingToolSettings.MappingPieMenuSize );

		var resetButton = new Editor.Button( "Reset to Defaults" );
		resetButton.Clicked += () =>
		{
			MappingToolSettings.ResetMappingPieMenuDefaults();
		};

		Layout.Add( sheet );
		Layout.Add( resetButton );
		Layout.AddStretchCell();
	}
}

// Add this new page class
internal sealed class PageAutoSave : Widget
{
	public PageAutoSave( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 32;
		Layout.Spacing = 16;

		Layout.Add( new Subtitle( "Auto-Save" ) );
		Layout.Add( new Editor.Label( "Automatically create backup saves at regular intervals" ) { WordWrap = true } );

		var sheet = new Editor.ControlSheet();

		sheet.AddProperty( () => MappingToolSettings.AutoSaveEnabled );
		sheet.AddProperty( () => MappingToolSettings.AutoSaveIntervalMinutes );
		sheet.AddProperty( () => MappingToolSettings.AutoSaveMaxBackups );
		sheet.AddProperty( () => MappingToolSettings.AutoSaveShowNotification );

		Layout.Add( sheet );

		// Force save button
		var saveNowButton = new Editor.Button( "Save Backup Now", "save" );
		saveNowButton.Clicked += () => AutoSave.ForceAutoSave();
		Layout.Add( saveNowButton );

		// Open folder button
		var openFolderButton = new Editor.Button( "Open Autosave Folder", "folder_open" );
		openFolderButton.Clicked += OpenAutoSaveFolder;
		Layout.Add( openFolderButton );

		var resetButton = new Editor.Button( "Reset to Defaults" );
		resetButton.Clicked += () => MappingToolSettings.ResetAutoSaveDefaults();
		Layout.Add( resetButton );

		Layout.AddStretchCell();
	}

	private void OpenAutoSaveFolder()
	{
		var session = SceneEditorSession.Active;
		if ( session?.Scene?.Source?.ResourcePath == null )
		{
			Log.Info( "No active scene" );
			return;
		}

		var sceneDirectory = Path.GetDirectoryName( session.Scene.Source.ResourcePath );
		var autoSaveFolder = Path.Combine( sceneDirectory, "autosave" );
		var fullPath = Editor.FileSystem.ProjectTemporary.GetFullPath( autoSaveFolder );

		// Create the folder if it doesn't exist
		if ( !Directory.Exists( fullPath ) )
		{
			Directory.CreateDirectory( fullPath );
		}

		System.Diagnostics.Process.Start( "explorer.exe", fullPath );
	}
}

/// <summary>
/// Settings for mapping tools including pie menu keybinds
/// </summary>
public static class MappingToolSettings
{
	private const string PreferencePrefix = "MappingTools.";

	// Render Pie Menu Settings
	[Title( "Mouse Button" )]
	[Description( "Mouse button to open the render pie menu" )]
	public static MouseButtons PieMenuButton
	{
		get => (MouseButtons)EditorCookie.Get( PreferencePrefix + "PieMenuButton", (int)MouseButtons.Forward );
		set => EditorCookie.Set( PreferencePrefix + "PieMenuButton", (int)value );
	}

	[Title( "Use Modifier Key" )]
	[Description( "Whether to require a modifier key to open the render pie menu" )]
	public static bool PieMenuUseModifier
	{
		get => EditorCookie.Get( PreferencePrefix + "PieMenuUseModifier", false );
		set => EditorCookie.Set( PreferencePrefix + "PieMenuUseModifier", value );
	}

	[Title( "Modifier Key" )]
	[Description( "Modifier key to open the render pie menu with" )]
	public static KeyCode PieMenuModifierKey
	{
		get => (KeyCode)EditorCookie.Get( PreferencePrefix + "PieMenuModifierKey", (int)KeyCode.Control );
		set => EditorCookie.Set( PreferencePrefix + "PieMenuModifierKey", (int)value );
	}

	[Title( "Menu Size" )]
	[Description( "Radius of the render pie menu in pixels" )]
	[Range( 100, 400 )]
	public static float PieMenuSize
	{
		get => EditorCookie.Get( PreferencePrefix + "PieMenuSize", 180f );
		set => EditorCookie.Set( PreferencePrefix + "PieMenuSize", value );
	}

	// Mapping Pie Menu Settings
	[Title( "Mouse Button" )]
	[Description( "Mouse button to open the mapping mode pie menu" )]
	public static MouseButtons MappingPieMenuButton
	{
		get => (MouseButtons)EditorCookie.Get( PreferencePrefix + "MappingPieMenuButton", (int)MouseButtons.Back );
		set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuButton", (int)value );
	}

	[Title( "Use Modifier Key" )]
	[Description( "Whether to require a modifier key to open the mapping pie menu" )]
	public static bool MappingPieMenuUseModifier
	{
		get => EditorCookie.Get( PreferencePrefix + "MappingPieMenuUseModifier", false );
		set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuUseModifier", value );
	}

	[Title( "Modifier Key" )]
	[Description( "Modifier key to open the mapping pie menu with" )]
	public static KeyCode MappingPieMenuModifierKey
	{
		get => (KeyCode)EditorCookie.Get( PreferencePrefix + "MappingPieMenuModifierKey", (int)KeyCode.Control );
		set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuModifierKey", (int)value );
	}

	[Title( "Menu Size" )]
	[Description( "Radius of the mapping pie menu in pixels" )]
	[Range( 100, 400 )]
	public static float MappingPieMenuSize
	{
		get => EditorCookie.Get( PreferencePrefix + "MappingPieMenuSize", 180f );
		set => EditorCookie.Set( PreferencePrefix + "MappingPieMenuSize", value );
	}

	// Material Gallery Settings
	[Title( "Default Organization" )]
	[Description( "Primary organization to search for materials" )]
	public static string DefaultOrganization
	{
		get => EditorCookie.Get( PreferencePrefix + "DefaultOrganization", "facepunch" );
		set => EditorCookie.Set( PreferencePrefix + "DefaultOrganization", value );
	}

	public static IEnumerable<string> AdditionalOrganizations
	{
		get
		{
			var json = EditorCookie.Get( PreferencePrefix + "AdditionalOrganizations", "[]" );
			return Json.Deserialize<List<string>>( json ) ?? new List<string>();
		}
		set
		{
			var json = Json.Serialize( value );
			EditorCookie.Set( PreferencePrefix + "AdditionalOrganizations", json );
		}
	}

	public static void AddOrganization( string org )
	{
		var orgs = AdditionalOrganizations.ToList();
		if ( !orgs.Contains( org, StringComparer.OrdinalIgnoreCase ) )
		{
			orgs.Add( org );
			AdditionalOrganizations = orgs;
		}
	}

	public static void RemoveOrganization( string org )
	{
		var orgs = AdditionalOrganizations.ToList();
		orgs.RemoveAll( o => o.Equals( org, StringComparison.OrdinalIgnoreCase ) );
		AdditionalOrganizations = orgs;
	}

	public static void ResetPieMenuDefaults()
	{
		PieMenuButton = MouseButtons.Forward;
		PieMenuUseModifier = false;
		PieMenuModifierKey = KeyCode.Control;
		PieMenuSize = 180f;
	}

	public static void ResetMappingPieMenuDefaults()
	{
		MappingPieMenuButton = MouseButtons.Back;
		MappingPieMenuUseModifier = false;
		MappingPieMenuModifierKey = KeyCode.Control;
		MappingPieMenuSize = 180f;
	}

	public static void ResetMaterialGalleryDefaults()
	{
		DefaultOrganization = "facepunch";
		AdditionalOrganizations = new List<string>();
	}

	// Add these to MappingToolSettings class:

	// Auto-Save Settings
	[Title( "Enable Auto-Save" )]
	[Description( "Automatically save backups of your scene at regular intervals" )]
	public static bool AutoSaveEnabled
	{
		get => EditorCookie.Get( PreferencePrefix + "AutoSaveEnabled", true );
		set => EditorCookie.Set( PreferencePrefix + "AutoSaveEnabled", value );
	}

	[Title( "Interval (Minutes)" )]
	[Description( "How often to create a backup save" )]
	[Range( 1, 60 )]
	public static float AutoSaveIntervalMinutes
	{
		get => EditorCookie.Get( PreferencePrefix + "AutoSaveIntervalMinutes", 5f );
		set => EditorCookie.Set( PreferencePrefix + "AutoSaveIntervalMinutes", value );
	}

	[Title( "Maximum Backups" )]
	[Description( "Maximum number of backup files to keep per scene (0 = unlimited)" )]
	[Range( 0, 50 )]
	public static int AutoSaveMaxBackups
	{
		get => EditorCookie.Get( PreferencePrefix + "AutoSaveMaxBackups", 10 );
		set => EditorCookie.Set( PreferencePrefix + "AutoSaveMaxBackups", value );
	}

	[Title( "Show Notification" )]
	[Description( "Show a toast notification when auto-save completes" )]
	public static bool AutoSaveShowNotification
	{
		get => EditorCookie.Get( PreferencePrefix + "AutoSaveShowNotification", true );
		set => EditorCookie.Set( PreferencePrefix + "AutoSaveShowNotification", value );
	}

	// Double-Click Settings
	[Title( "Double-Click to Mesh Mode" )]
	[Description( "Double-click on a GameObject with MeshComponent to enter Mesh Tool mode" )]
	public static bool DoubleClickToMeshMode
	{
		get => EditorCookie.Get( PreferencePrefix + "DoubleClickToMeshMode", false );
		set => EditorCookie.Set( PreferencePrefix + "DoubleClickToMeshMode", value );
	}

	public static void ResetAutoSaveDefaults()
	{
		AutoSaveEnabled = true;
		AutoSaveIntervalMinutes = 5f;
		AutoSaveMaxBackups = 10;
		AutoSaveShowNotification = true;
	}
}
using System;

namespace Sandbox;
[Title("Particle Ring Emitter Even")]
[Category( "Particles" )]	
[Description("Let's you ensure that the particles are placed evenly around the ring when emitted. Simply check the \"Even Angle\" checkbox and you are set !")]
public class ParticleRingEmitterEven : ParticleEmitter
{
	[Property] public ParticleFloat Radius { get; set; } = 50.0f;
	[Property] public ParticleFloat Thickness { get; set; } = 10.0f;
	[Property, Range( 0, 360 )] public ParticleFloat AngleStart { get; set; } = 0.0f;
	[Property, Range( 0, 360 )] public ParticleFloat Angle { get; set; } = 360.0f;
	[Property, Range( 0, 1 )] public ParticleFloat Flatness { get; set; } = 0.0f;
	[Property, Range( -100, 100 )] public ParticleFloat VelocityFromCenter { get; set; } = 0.0f;
	[Property, Range( -100, 100 )] public ParticleFloat VelocityFromRing { get; set; } = 0.0f;
	[Property] public bool EvenAngle { get; set; } = false;

	private float _angleStep = 0.0f;

	protected override void OnUpdate()
	{

		
	}
	
	public override bool Emit( ParticleEffect target )
	{
		
		if ( target.Particles.Count == 0 )
		{
			_angleStep = 0.0f;
		}
		
		var angle = 0f;
		if ( !EvenAngle )
		{
			angle = Random.Shared.Float( 0, Angle.Evaluate( Delta, EmitRandom ).DegreeToRadian() );
			angle += AngleStart.Evaluate( Delta, EmitRandom ).DegreeToRadian();
		}
		else
		{
			angle = _angleStep;
			AngleStepBurst();
		}

		var x = MathF.Sin( angle );
		var y = MathF.Cos( angle );

		var size = new Vector3( x, y, 0 ) * Radius.Evaluate( Delta, 0 );
		var ringOffset = Vector3.Zero;

		var thickness = Thickness.Evaluate( Delta, EmitRandom );

		if ( thickness > 0 )
		{
			ringOffset = Vector3.Random * thickness;
			ringOffset.z *= (1 - Flatness.Evaluate( Delta, EmitRandom ));

			size += ringOffset;
		}

		size = (size * WorldScale) * WorldRotation;

		var p = target.Emit( WorldPosition + size, Delta );
		if ( p is not null )
		{
			var velFromCenter = VelocityFromCenter.Evaluate( Delta, EmitRandom );
			if ( velFromCenter != 0 )
			{
				p.Velocity += (size.Normal * velFromCenter);
			}

			var velFromRing = VelocityFromRing.Evaluate( Delta, EmitRandom );
			if ( velFromRing != 0 )
			{
				ringOffset = (ringOffset * WorldScale) * WorldRotation;
				p.Velocity += (ringOffset.Normal * velFromRing);
			}
		}

		return true;
	}

	private void AngleStepBurst()
	{
		
		
		_angleStep += Angle.Evaluate( Delta, EmitRandom ).DegreeToRadian() / Burst ;
		
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Sandbox;

namespace Sandbox;

[Title( "Shader Particle Model Renderer" )]
[Category( "Particles" )]
[Description(
	"Adds the \"Particle Shader\" feature which let's you set shader parameters on the particles model using the particle system !" )]
public class ShaderParticleModelRenderer : ParticleController, Component.ExecuteInEditor
{
	private ParticleModelRenderer _particleModelRenderer { get; set; } = new ParticleModelRenderer();


	/**
	 *  These are the dictionaries used to set the values for the render attributes in the particle system
	 */
	[Property, FeatureEnabled("ParticleShader")]
	public bool ParticleShaderEnabled { get; set; } = true;
	
	
	[Property, Group( "ColorParameters" ), Feature( "ParticleShader" )]
	public Dictionary<String, ParticleGradient> Colors { get; set; }

	[Property, Group( "FloatParameters" ), Feature( "ParticleShader" )]
	public Dictionary<String, ParticleFloat> Floats { get; set; }

	[Property, Group( "Float2Parameters" ), Feature( "ParticleShader" )]
	public Dictionary<String, Vector2> Floats2 { get; set; }

	[Property, Group( "Float4Parameters" ), Feature( "ParticleShader" )]
	public Dictionary<String, Vector4> Floats4 { get; set; }

	[Property, Group( "Textures" ), Feature( "ParticleShader" )]
	public Dictionary<String, Texture> Textures { get; set; }

	[Property, Group( "DynamicCombos" ), Feature( "ParticleShader" )]
	public Dictionary<String, int> DynamicCombos { get; set; }
	

	
	

	[RequireComponent] public new ParticleEffect ParticleEffect { get; set; }

	[Property, Order( -100 ), InlineEditor( Label = false ), Group( "Advanced Rendering", StartFolded = true )]
	public RenderOptions RenderOptions => _particleModelRenderer.RenderOptions;
	
	
	protected override void OnStart()
	{
		Floats = Floats == null ? new Dictionary<string, ParticleFloat>() : Floats;
		Floats2 = Floats2 == null ? new Dictionary<string, Vector2>() : Floats2;
		Floats4 = Floats4 == null ? new Dictionary<string, Vector4>() : Floats4;
		Textures = Textures == null ? new Dictionary<string, Texture>() : Textures;
		Colors = Colors == null ? new Dictionary<String, ParticleGradient>() : Colors;
		DynamicCombos = DynamicCombos == null ? new Dictionary<string, int>() : DynamicCombos;
		
		if( ParticleShaderEnabled ) ReadAttributes();
	}
	
	[Button, Feature("ParticleShader")]
	private void ReadAttributes()
	{
		if ( MaterialOverride == null || !FileSystem.Mounted.FileExists( MaterialOverride.Shader.ResourcePath )) return;
		AttributesParser<ParticleFloat, ParticleGradient> parser = new AttributesParser<ParticleFloat, ParticleGradient>(new ParticleAttributeTypeSet());
		parser.Floats = Floats;
		parser.Floats2 = Floats2;
		parser.Floats4 = Floats4;
		parser.Textures = Textures;
		parser.DynamicCombos = DynamicCombos;
		parser.Colors = Colors;
		parser.ParseAttributes( MaterialOverride.Shader.ResourcePath );
		
	}

	public sealed class ModelEntry
	{
		private Model _model;

		[KeyProperty]
		public Model Model
		{
			get => _model;
			set
			{
				if ( _model == value )
					return;

				_model = value;

				MaterialGroup = default;
				BodyGroups = _model?.DefaultBodyGroupMask ?? default;
			}
		}

		[Model.MaterialGroup, ShowIf( nameof(HasMaterialGroups), true )]
		public string MaterialGroup { get; set; }

		[Model.BodyGroupMask, ShowIf( nameof(HasBodyGroups), true )]
		public ulong BodyGroups { get; set; }

		[Hide, JsonIgnore] public bool HasMaterialGroups => Model?.MaterialGroupCount > 0;

		[Hide, JsonIgnore] public bool HasBodyGroups => Model?.BodyParts.Sum( x => x.Choices.Count ) > 1;

		public static implicit operator ModelEntry( Model model ) => new() { Model = model };
	}

	[Hide, Obsolete( "Use Choices" )] public List<Model> Models { get; set; } = new();

	[Property] public List<ModelEntry> Choices { get; set; } = new List<ModelEntry> { Model.Cube };

	[Property] public Material MaterialOverride { get; set; }

	[Property, Feature( "ScaleXYZ" )] public ParticleFloat ScaleX { get; set; } = 1;
	[Property, Feature( "ScaleXYZ" )] public ParticleFloat ScaleY { get; set; } = 1;
	[Property, Feature( "ScaleXYZ" )] public ParticleFloat ScaleZ { get; set; } = 1;

	[Property, FeatureEnabled( "ScaleXYZ" )]
	public bool ApplyScaleXYZ { get; set; } = true;

	[Property] public float Scale { get; set; } = 1;
	
	[Property] public bool CastShadows { get; set; } = true;

	[Property] public Allignement Allignement { get; set; }

	protected override void OnParticleCreated( Particle p )
	{
		var particleModel = new CustomParticleModel( this );
		p.AddListener( particleModel, this );
		
	}

	public override int ComponentVersion => 1;

	[JsonUpgrader( typeof(ParticleModelRenderer), 1 )]
	static void Upgrader_v1( JsonObject obj )
	{
		if ( obj.TryGetPropertyValue( "Models", out var node ) )
		{
			var choices = new JsonArray();

			foreach ( var model in node.AsArray() )
			{
				if ( model is null )
					continue;

				choices.Add( new JsonObject { ["Model"] = model.ToString() } );
			}

			obj["Choices"] = choices;
			obj.Remove( "Models" );
		}
	}
	
	
}

public enum Allignement
{
	SimulationSpace,
	FaceCamera,
	FaceVelocity,
	
}





public class CustomParticleModel : Particle.BaseListener
{
	public ShaderParticleModelRenderer Renderer;

	public SceneObject so;

	private ParticleAttributesSetter _particleAttributesSetter;

	public CustomParticleModel( ShaderParticleModelRenderer renderer )
	{
		Renderer = renderer;
	}

	public override void OnEnabled( Particle p )
	{
		var entry = Random.Shared.FromList( Renderer.Choices );
		var model = entry?.Model;
		so = new SceneObject( Renderer.Scene.SceneWorld, model ?? Model.Cube );
		so.Batchable = false;
		if ( model is not null )
		{
			so.MeshGroupMask = entry.BodyGroups;
			so.SetMaterialGroup( entry.MaterialGroup );
		}

		if ( !Renderer.ParticleShaderEnabled ) return;
		_particleAttributesSetter = new ParticleAttributesSetter( so.Attributes, p );
		SetRenderAttributes();
	}

	public override void OnDisabled( Particle p )
	{
		if ( !so.IsValid() ) return;
		so.Delete();
	}

	public override void OnUpdate( Particle p, float dt )
	{
		if ( !so.IsValid() ) return;


		var angles = ComputeRotation( p );
		
		var scale = p.Size * Renderer.WorldScale;
		if ( Renderer.ApplyScaleXYZ )
		{
			scale *= EvaluateScale( p );
		}

		so.Transform = new Transform( p.Position, angles, scale * Renderer.Scale );
		so.ColorTint = p.Color.WithAlphaMultiplied( p.Alpha );
		so.Flags.CastShadows = Renderer.CastShadows;
		so.SetMaterialOverride( Renderer.MaterialOverride );
		
		if(Renderer.ParticleShaderEnabled) _particleAttributesSetter.SetAttributes();
		

		if ( Renderer.RenderOptions != null )
		{
			Renderer.RenderOptions.Apply( so );
		}
	}

	


	private Vector3 EvaluateScale( Particle p )
	{
		
		var scaleX = Renderer.ScaleX.Evaluate( p, 6211 );
		var scaleY = Renderer.ScaleY.Evaluate( p, 6211 );
		var scaleZ = Renderer.ScaleZ.Evaluate( p, 6211 );
		return new Vector3( scaleX, scaleY, scaleZ );
	}


	private Angles ComputeRotation(Particle p)
	{
		var angles = new Rotation();
		switch ( Renderer.Allignement )
		{
			case Allignement.FaceCamera :
				if ( Renderer.Scene.Camera == null ) break;
				var dir = Renderer.Scene.Camera.WorldPosition - p.Position;
				angles = Rotation.LookAt( dir, Vector3.Up ) * p.Angles.ToRotation();
				break;
			case Allignement.FaceVelocity :
				angles = Rotation.LookAt( p.Velocity.Normal, Vector3.Up ) * p.Angles.ToRotation();
				break;
			case Allignement.SimulationSpace :
				angles = Renderer.ParticleEffect.LocalSpace.Evaluate( p,65373 ) <= 1 ? Renderer.WorldRotation.Angles() : Rotation.Identity.Angles(); 
				angles *= p.Angles;
				break;
		}
		return angles;
	}
	private void SetRenderAttributes()
	{
		_particleAttributesSetter.Floats = Renderer.Floats;
		_particleAttributesSetter.Floats2 = Renderer.Floats2;
		_particleAttributesSetter.Floats4 = Renderer.Floats4;
		_particleAttributesSetter.Textures = Renderer.Textures;
		_particleAttributesSetter.Colors = Renderer.Colors;
		_particleAttributesSetter.DynamicCombos = Renderer.DynamicCombos;
		_particleAttributesSetter.SetAttributes();
	}
}
namespace Sandbox;
// The code in this file is ai generated
public class ParticleAttributeTypeSet : IAttributeTypeSet<ParticleFloat, ParticleGradient>
{
	public ParticleFloat GetDefaultFloat() => new ParticleFloat();
	public ParticleGradient GetDefaultColor() => new ParticleGradient();
}
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sandbox;
[Title("ModelShaderAttributes")]
[Category("Shaders")]
[Description("Let's you set the render attributes of the scene object of a ModelRenderer")]
public sealed class ModelShaderAttributes : Component, Component.ExecuteInEditor
{
	[Property] private ModelRenderer ModelRenderer { get; set; }
	[Property, Group( "Floats1" )] public Dictionary<String, float> Floats { get; set; } 
	[Property, Group("Floats2")] public Dictionary<String, Vector2> Floats2 { get; set; }
	[Property, Group("Colors")] public Dictionary<String, Color> Colors {get; set;}
	[Property, Group("Textures")] public Dictionary<String, Texture> Textures { get; set; }
	
	[Property, Group("Floats4")] public Dictionary<String, Vector4> Floats4 { get; set; }
	
	[Property, Group("DynamicCombos")] public Dictionary<String, int> DynamicCombos { get; set; }
	
	[Property] public bool Batchable { get; set; } = true;
	
	
	protected override void OnStart()
	{
		ModelRenderer.SceneObject.Batchable = Batchable;
		Floats = Floats == null ? new Dictionary<string, float>() : Floats;
		Floats2 = Floats2 == null ? new Dictionary<string, Vector2>() : Floats2;
		Floats4 = Floats4 == null ? new Dictionary<string, Vector4>() : Floats4;
		Textures = Textures == null ? new Dictionary<string, Texture>() : Textures;
		Colors = Colors == null ? new Dictionary<String, Color>() : Colors;
		DynamicCombos = DynamicCombos == null ? new Dictionary<string, int>() : DynamicCombos;
		
	}
	[Button]
	private void ReadAttributes()
	{
		if ( ModelRenderer.MaterialOverride == null || !FileSystem.Mounted.FileExists( ModelRenderer.MaterialOverride.Shader.ResourcePath )) return;
		AttributesParser<float,Color> parser = new AttributesParser<float,Color>(new NativeAttributeTypeSet());
		parser.Floats = Floats;
		parser.Floats2 = Floats2;
		parser.Floats4 = Floats4;
		parser.Textures = Textures;
		parser.DynamicCombos = DynamicCombos;
		parser.Colors = Colors;
		parser.ParseAttributes( ModelRenderer.MaterialOverride.Shader.ResourcePath );
		
	}
	protected override void OnUpdate()
	{
		SetColorAttributes();
		SetFloatAttributes();
		SetFloat2Attributes();
		SetTexturesAttributes();
		SetFloat4Attributes();
		SetDynamicCombos();
	}

	private void SetDynamicCombos()
	{
		foreach ( var dynamicCombo in DynamicCombos )
		{
			ModelRenderer.SceneObject.Attributes.SetCombo( dynamicCombo.Key, dynamicCombo.Value );
		}
	}

	private void SetFloat4Attributes()
	{
		foreach ( var float4Attribute in Floats4 )
		{
			ModelRenderer.SceneObject.Attributes.Set( float4Attribute.Key, float4Attribute.Value );
		}
	}

	private void SetTexturesAttributes()
	{
		foreach ( var textureAttribute in Textures )
		{
			ModelRenderer.SceneObject.Attributes.Set( textureAttribute.Key, textureAttribute.Value );
		}
	}


	private void SetColorAttributes()
	{
		foreach ( var colorAttribute in Colors )
		{
			ModelRenderer.SceneObject.Attributes.Set( colorAttribute.Key, colorAttribute.Value );
		}
	}

	private void SetFloatAttributes()
	{
		foreach ( var float1 in Floats )
		{
			ModelRenderer.SceneObject.Attributes.Set( float1.Key, float1.Value );
		}
	}

	private void SetFloat2Attributes()
	{
		foreach ( var float2 in Floats2 )
		{
			ModelRenderer.SceneObject.Attributes.Set( float2.Key, float2.Value );
		}
	}
}


public sealed class MVCitizenAnimation : Component, Component.ExecuteInEditor
{
	[Property] public SkinnedModelRenderer Target { get; set; }

	[Property] public GameObject EyeSource { get; set; }

	[Property] public GameObject LookAtObject { get; set; }

	[Property, Range( 0.5f, 1.5f )] public float Height { get; set; } = 1.0f;


	[Property] public GameObject IkLeftHand { get; set; }
	[Property] public GameObject IkRightHand { get; set; }
	[Property] public GameObject IkLeftFoot { get; set; }
	[Property] public GameObject IkRightFoot { get; set; }

	[Property] public HoldTypes CurrentHoldType { get; set; }

	public float SkidAmount { get; set; }

	[Property, Range( 0, 10 )] public int FacesOverride { get; set; }

	protected override void OnUpdate()
	{
		if ( LookAtObject.IsValid() )
		{
			var eyePos = GetEyeWorldTransform.Position;

			var dir = (LookAtObject.WorldPosition - eyePos).Normal;
			WithLook( dir, 1, 0.5f, 0.1f );
		}

		Target.Set( "scale_height", Height );

		// SetIk( "left_hand", ... );
		// SetIk( "right_hand", ... );

		if ( IkLeftHand.IsValid() && IkLeftHand.Active ) SetIk( "hand_left", IkLeftHand.Transform.World );
		else ClearIk( "hand_left" );

		if ( IkRightHand.IsValid() && IkRightHand.Active ) SetIk( "hand_right", IkRightHand.Transform.World );
		else ClearIk( "hand_right" );

		if ( IkLeftFoot.IsValid() && IkLeftFoot.Active ) SetIk( "foot_left", IkLeftFoot.Transform.World );
		else ClearIk( "foot_left" );

		if ( IkRightFoot.IsValid() && IkRightFoot.Active ) SetIk( "foot_right", IkRightFoot.Transform.World );
		else ClearIk( "foot_right" );

		HoldType = CurrentHoldType;

		FaceOverride = FacesOverride;
	}

	public void SetIk( string name, Transform tx )
	{
		// convert local to model
		tx = Target.Transform.World.ToLocal( tx );

		Target.Set( $"ik.{name}.enabled", true );
		Target.Set( $"ik.{name}.position", tx.Position );
		Target.Set( $"ik.{name}.rotation", tx.Rotation );
	}

	public void ClearIk( string name )
	{
		Target.Set( $"ik.{name}.enabled", false );
	}

	public Transform GetEyeWorldTransform
	{
		get
		{
			if ( EyeSource.IsValid() ) return EyeSource.Transform.World;

			return Transform.World;
		}
	}


	/// <summary>
	/// Have the player look at this point in the world
	/// </summary>
	public void WithLook( Vector3 lookDirection, float eyesWeight = 1.0f, float headWeight = 1.0f, float bodyWeight = 1.0f )
	{
		Target.SetLookDirection( "aim_eyes", lookDirection );
		Target.SetLookDirection( "aim_head", lookDirection );
		Target.SetLookDirection( "aim_body", lookDirection );

		AimEyesWeight = eyesWeight;
		AimHeadWeight = headWeight;
		AimBodyWeight = bodyWeight;
	}

	public void WithVelocity( Vector3 Velocity )
	{
		var dir = Velocity;
		var forward = Target.WorldRotation.Forward.Dot( dir );
		var sideward = Target.WorldRotation.Right.Dot( dir );

		var angle = MathF.Atan2( sideward, forward ).RadianToDegree().NormalizeDegrees();

		Target.Set( "move_direction", angle );
		Target.Set( "move_speed", Velocity.Length );
		Target.Set( "move_groundspeed", Velocity.WithZ( 0 ).Length );
		Target.Set( "move_y", sideward );
		Target.Set( "move_x", forward );
		Target.Set( "move_z", Velocity.z );
	}

	public void WithWishVelocity( Vector3 Velocity )
	{
		var dir = Velocity;
		var forward = Target.WorldRotation.Forward.Dot( dir );
		var sideward = Target.WorldRotation.Right.Dot( dir );

		var angle = MathF.Atan2( sideward, forward ).RadianToDegree().NormalizeDegrees();

		Target.Set( "wish_direction", angle );
		Target.Set( "wish_speed", Velocity.Length );
		Target.Set( "wish_groundspeed", Velocity.WithZ( 0 ).Length );
		Target.Set( "wish_y", sideward );
		Target.Set( "wish_x", forward );
		Target.Set( "wish_z", Velocity.z );
	}

	public float CheckForGroundAngle()
	{
		var trace = Scene.Trace.Ray( Target.WorldPosition + Vector3.Up * 2, Target.WorldPosition + Vector3.Down * 6 )
			.WithoutTags( "player", "collider" )
			.Radius( 8 )
			.Run();

		//	Gizmo.Draw.Color = Color.Red;
		//	Gizmo.Draw.Line( Target.WorldPosition + Vector3.Up * 2, Target.WorldPosition + Vector3.Down * 6 );

		if ( !trace.Hit )
			return 0;

		return trace.Normal.Angle( Vector3.Up );
	}

	public void SpecialMenu(bool menu)
	{
		var useidle = menu ? 1 : 0;
		Target.Set( "special_idle_states", useidle );
	}

	public Rotation AimAngle
	{
		set
		{
			value = Target.WorldRotation.Inverse * value;
			var ang = value.Angles();

			Target.Set( "aim_body_pitch", ang.pitch );
			Target.Set( "aim_body_yaw", ang.yaw );
		}
	}

	public float AimEyesWeight
	{
		get => Target.GetFloat( "aim_eyes_weight" );
		set => Target.Set( "aim_eyes_weight", value );
	}

	public float AimHeadWeight
	{
		get => Target.GetFloat( "aim_head_weight" );
		set => Target.Set( "aim_head_weight", value );
	}

	public float AimBodyWeight
	{
		get => Target.GetFloat( "aim_body_weight" );
		set => Target.Set( "aim_body_weight", value );
	}


	public float FootShuffle
	{
		get => Target.GetFloat( "move_shuffle" );
		set => Target.Set( "move_shuffle", value );
	}

	public float DuckLevel
	{
		get => Target.GetFloat( "duck" );
		set => Target.Set( "duck", value );
	}

	public float SkidLevel
	{
		get => Target.GetFloat( "skid" );
		set => Target.Set( "skid", value );
	}

	public float VoiceLevel
	{
		get => Target.GetFloat( "voice" );
		set => Target.Set( "voice", value );
	}

	public bool IsSitting
	{
		get => Target.GetBool( "b_sit" );
		set => Target.Set( "b_sit", value );
	}

	public bool IsGrounded
	{
		get => Target.GetBool( "b_grounded" );
		set => Target.Set( "b_grounded", value );
	}

	public bool IsSwimming
	{
		get => Target.GetBool( "b_swim" );
		set => Target.Set( "b_swim", value );
	}

	public bool IsClimbing
	{
		get => Target.GetBool( "b_climbing" );
		set => Target.Set( "b_climbing", value );
	}

	public bool IsNoclipping
	{
		get => Target.GetBool( "b_noclip" );
		set => Target.Set( "b_noclip", value );
	}

	public bool IsWeaponLowered
	{
		get => Target.GetBool( "b_weapon_lower" );
		set => Target.Set( "b_weapon_lower", value );
	}

	public enum HoldTypes
	{
		None,
		Pistol,
		Rifle,
		Shotgun,
		HoldItem,
		Punch,
		Swing,
		RPG
	}

	public HoldTypes HoldType
	{
		get => (HoldTypes)Target.GetInt( "holdtype" );
		set => Target.Set( "holdtype", (int)value );
	}

	public enum Hand
	{
		Both,
		Right,
		Left
	}

	public Hand Handedness
	{
		get => (Hand)Target.GetInt( "holdtype_handedness" );
		set => Target.Set( "holdtype_handedness", (int)value );
	}

	public void TriggerJump()
	{
		Target.Set( "b_jump", true );
	}

	public void TriggerDeploy()
	{
		Target.Set( "b_deploy", true );
	}

	public enum MoveStyles
	{
		Auto,
		Walk,
		Run
	}

	/// <summary>
	/// We can force the model to walk or run, or let it decide based on the speed.
	/// </summary>
	public MoveStyles MoveStyle
	{
		get => (MoveStyles)Target.GetInt( "move_style" );
		set => Target.Set( "move_style", (int)value );
	}

	public enum SpecialMoveStyle
	{
		None,
		LedgeGrab,
		Roll,
		Slide
	}

	public SpecialMoveStyle SpecialMove
	{
		get => (SpecialMoveStyle)Target.GetInt( "special_movement_states" );
		set => Target.Set( "special_movement_states", (int)value );
	}

	public int FaceOverride
	{
		get => Target.GetInt( "face_override" );
		set => Target.Set( "face_override", value );
	}
}
using Sandbox;
using Editor;
using static Sandbox.ClothingContainer;

[Title( "Clothing Dresser" )]
[Category( "Clothing" )]
[Icon( "checkroom", "blue", "white" )]
public sealed class ModelViewerClothingDresser : Component
{
	[Property] SkinnedModelRenderer Source { get; set; }
	[Property] List<Clothing> ClothingList { get ; set; } = new();
	ClothingContainer Container { get; set; } = new ClothingContainer();
	public List<SceneModel> Dressed { get; private set; }

	//Hair Tint
	[Property] Gradient HairTintGradient { get; set; } = new Gradient( new Gradient.ColorFrame( 0.0f, Color.White ), new Gradient.ColorFrame( 0.16f, "#FCC88C" ), new Gradient.ColorFrame(0.34f, "#A57E6A" ), new Gradient.ColorFrame( 0.53f, "#A33900" ), new Gradient.ColorFrame( 0.75f, "#3A271D" ), new Gradient.ColorFrame( 1.0f, "#000000" ) );
	[Property] Color HairTint { get; set; }
	[Property, Range(0,1)] float HairTintValue { get; set; } = 0.4f;

	//Beard Tint
	[Property] Gradient BeardTintGradient { get; set; } = new Gradient( new Gradient.ColorFrame( 0.0f, Color.White ), new Gradient.ColorFrame( 0.16f, "#FCC88C" ), new Gradient.ColorFrame( 0.34f, "#A57E6A" ), new Gradient.ColorFrame( 0.53f, "#A33900" ), new Gradient.ColorFrame( 0.75f, "#3A271D" ), new Gradient.ColorFrame( 1.0f, "#000000" ) );
	[Property] Color BeardTint { get; set; } = Color.White;
	[Property, Range(0,1)] float BeardTintValue { get; set; } = 0.4f;

	protected override void OnStart()
	{
		if ( Source is null )
			return;

		if ( ClothingList is null )
			return;
	
		foreach ( var clothing in ClothingList )
		{
			if ( clothing is null )
				continue;
			var entry = new ClothingEntry( clothing );
			if ( Container.Clothing.Contains( entry ) )
				continue;
			
			Container.Clothing.Add( entry );
		}

		Container.Apply( Source );

		//Find the hair model
		foreach ( var model in GameObject.Children )
		{			
			var mod = model.Components.Get<SkinnedModelRenderer>();

			if( mod is null )
				continue;
			
			if ( model.Name.Contains( "hair" ) || mod.Model.ResourcePath.Contains("hair") && mod.Model.MorphCount <= 1 )
			{
				var hair = model.Components.Get<SkinnedModelRenderer>();
				hair.Tint = HairTint;
			}

			if ( mod.Model.MorphCount >= 1 )
			{
				var beard = model.Components.Get<SkinnedModelRenderer>();
				beard.Tint = BeardTint;
				Log.Info( "Beard Tint: " );
			}
		}
	}
}
using Sandbox;
using Editor;
using static Sandbox.ClothingContainer;

[Title( "Clothing Dresser" )]
[Category( "Clothing" )]
[Icon( "checkroom", "blue", "white" )]
public sealed class ModelViewerClothingDresser : Component
{
	[Property] SkinnedModelRenderer Source { get; set; }
	[Property] List<Clothing> ClothingList { get ; set; } = new();
	ClothingContainer Container { get; set; } = new ClothingContainer();
	public List<SceneModel> Dressed { get; private set; }

	//Hair Tint
	[Property] Gradient HairTintGradient { get; set; } = new Gradient( new Gradient.ColorFrame( 0.0f, Color.White ), new Gradient.ColorFrame( 0.16f, "#FCC88C" ), new Gradient.ColorFrame(0.34f, "#A57E6A" ), new Gradient.ColorFrame( 0.53f, "#A33900" ), new Gradient.ColorFrame( 0.75f, "#3A271D" ), new Gradient.ColorFrame( 1.0f, "#000000" ) );
	[Property] Color HairTint { get; set; }
	[Property, Range(0,1)] float HairTintValue { get; set; } = 0.4f;

	//Beard Tint
	[Property] Gradient BeardTintGradient { get; set; } = new Gradient( new Gradient.ColorFrame( 0.0f, Color.White ), new Gradient.ColorFrame( 0.16f, "#FCC88C" ), new Gradient.ColorFrame( 0.34f, "#A57E6A" ), new Gradient.ColorFrame( 0.53f, "#A33900" ), new Gradient.ColorFrame( 0.75f, "#3A271D" ), new Gradient.ColorFrame( 1.0f, "#000000" ) );
	[Property] Color BeardTint { get; set; } = Color.White;
	[Property, Range(0,1)] float BeardTintValue { get; set; } = 0.4f;

	protected override void OnStart()
	{
		if ( Source is null )
			return;

		if ( ClothingList is null )
			return;
	
		foreach ( var clothing in ClothingList )
		{
			if ( clothing is null )
				continue;
			var entry = new ClothingEntry( clothing );
			if ( Container.Clothing.Contains( entry ) )
				continue;
			
			Container.Clothing.Add( entry );
		}

		Container.Apply( Source );

		//Find the hair model
		foreach ( var model in GameObject.Children )
		{			
			var mod = model.Components.Get<SkinnedModelRenderer>();

			if( mod is null )
				continue;
			
			if ( model.Name.Contains( "hair" ) || mod.Model.ResourcePath.Contains("hair") && mod.Model.MorphCount <= 1 )
			{
				var hair = model.Components.Get<SkinnedModelRenderer>();
				hair.Tint = HairTint;
			}

			if ( mod.Model.MorphCount >= 1 )
			{
				var beard = model.Components.Get<SkinnedModelRenderer>();
				beard.Tint = BeardTint;
				Log.Info( "Beard Tint: " );
			}
		}
	}
}


public sealed class MVCitizenAnimation : Component, Component.ExecuteInEditor
{
	[Property] public SkinnedModelRenderer Target { get; set; }

	[Property] public GameObject EyeSource { get; set; }

	[Property] public GameObject LookAtObject { get; set; }

	[Property, Range( 0.5f, 1.5f )] public float Height { get; set; } = 1.0f;


	[Property] public GameObject IkLeftHand { get; set; }
	[Property] public GameObject IkRightHand { get; set; }
	[Property] public GameObject IkLeftFoot { get; set; }
	[Property] public GameObject IkRightFoot { get; set; }

	[Property] public HoldTypes CurrentHoldType { get; set; }

	public float SkidAmount { get; set; }

	[Property, Range( 0, 10 )] public int FacesOverride { get; set; }

	protected override void OnUpdate()
	{
		if ( LookAtObject.IsValid() )
		{
			var eyePos = GetEyeWorldTransform.Position;

			var dir = (LookAtObject.WorldPosition - eyePos).Normal;
			WithLook( dir, 1, 0.5f, 0.1f );
		}

		Target.Set( "scale_height", Height );

		// SetIk( "left_hand", ... );
		// SetIk( "right_hand", ... );

		if ( IkLeftHand.IsValid() && IkLeftHand.Active ) SetIk( "hand_left", IkLeftHand.Transform.World );
		else ClearIk( "hand_left" );

		if ( IkRightHand.IsValid() && IkRightHand.Active ) SetIk( "hand_right", IkRightHand.Transform.World );
		else ClearIk( "hand_right" );

		if ( IkLeftFoot.IsValid() && IkLeftFoot.Active ) SetIk( "foot_left", IkLeftFoot.Transform.World );
		else ClearIk( "foot_left" );

		if ( IkRightFoot.IsValid() && IkRightFoot.Active ) SetIk( "foot_right", IkRightFoot.Transform.World );
		else ClearIk( "foot_right" );

		HoldType = CurrentHoldType;

		FaceOverride = FacesOverride;
	}

	public void SetIk( string name, Transform tx )
	{
		// convert local to model
		tx = Target.Transform.World.ToLocal( tx );

		Target.Set( $"ik.{name}.enabled", true );
		Target.Set( $"ik.{name}.position", tx.Position );
		Target.Set( $"ik.{name}.rotation", tx.Rotation );
	}

	public void ClearIk( string name )
	{
		Target.Set( $"ik.{name}.enabled", false );
	}

	public Transform GetEyeWorldTransform
	{
		get
		{
			if ( EyeSource.IsValid() ) return EyeSource.Transform.World;

			return Transform.World;
		}
	}


	/// <summary>
	/// Have the player look at this point in the world
	/// </summary>
	public void WithLook( Vector3 lookDirection, float eyesWeight = 1.0f, float headWeight = 1.0f, float bodyWeight = 1.0f )
	{
		Target.SetLookDirection( "aim_eyes", lookDirection );
		Target.SetLookDirection( "aim_head", lookDirection );
		Target.SetLookDirection( "aim_body", lookDirection );

		AimEyesWeight = eyesWeight;
		AimHeadWeight = headWeight;
		AimBodyWeight = bodyWeight;
	}

	public void WithVelocity( Vector3 Velocity )
	{
		var dir = Velocity;
		var forward = Target.WorldRotation.Forward.Dot( dir );
		var sideward = Target.WorldRotation.Right.Dot( dir );

		var angle = MathF.Atan2( sideward, forward ).RadianToDegree().NormalizeDegrees();

		Target.Set( "move_direction", angle );
		Target.Set( "move_speed", Velocity.Length );
		Target.Set( "move_groundspeed", Velocity.WithZ( 0 ).Length );
		Target.Set( "move_y", sideward );
		Target.Set( "move_x", forward );
		Target.Set( "move_z", Velocity.z );
	}

	public void WithWishVelocity( Vector3 Velocity )
	{
		var dir = Velocity;
		var forward = Target.WorldRotation.Forward.Dot( dir );
		var sideward = Target.WorldRotation.Right.Dot( dir );

		var angle = MathF.Atan2( sideward, forward ).RadianToDegree().NormalizeDegrees();

		Target.Set( "wish_direction", angle );
		Target.Set( "wish_speed", Velocity.Length );
		Target.Set( "wish_groundspeed", Velocity.WithZ( 0 ).Length );
		Target.Set( "wish_y", sideward );
		Target.Set( "wish_x", forward );
		Target.Set( "wish_z", Velocity.z );
	}

	public float CheckForGroundAngle()
	{
		var trace = Scene.Trace.Ray( Target.WorldPosition + Vector3.Up * 2, Target.WorldPosition + Vector3.Down * 6 )
			.WithoutTags( "player", "collider" )
			.Radius( 8 )
			.Run();

		//	Gizmo.Draw.Color = Color.Red;
		//	Gizmo.Draw.Line( Target.WorldPosition + Vector3.Up * 2, Target.WorldPosition + Vector3.Down * 6 );

		if ( !trace.Hit )
			return 0;

		return trace.Normal.Angle( Vector3.Up );
	}

	public void SpecialMenu(bool menu)
	{
		var useidle = menu ? 1 : 0;
		Target.Set( "special_idle_states", useidle );
	}

	public Rotation AimAngle
	{
		set
		{
			value = Target.WorldRotation.Inverse * value;
			var ang = value.Angles();

			Target.Set( "aim_body_pitch", ang.pitch );
			Target.Set( "aim_body_yaw", ang.yaw );
		}
	}

	public float AimEyesWeight
	{
		get => Target.GetFloat( "aim_eyes_weight" );
		set => Target.Set( "aim_eyes_weight", value );
	}

	public float AimHeadWeight
	{
		get => Target.GetFloat( "aim_head_weight" );
		set => Target.Set( "aim_head_weight", value );
	}

	public float AimBodyWeight
	{
		get => Target.GetFloat( "aim_body_weight" );
		set => Target.Set( "aim_body_weight", value );
	}


	public float FootShuffle
	{
		get => Target.GetFloat( "move_shuffle" );
		set => Target.Set( "move_shuffle", value );
	}

	public float DuckLevel
	{
		get => Target.GetFloat( "duck" );
		set => Target.Set( "duck", value );
	}

	public float SkidLevel
	{
		get => Target.GetFloat( "skid" );
		set => Target.Set( "skid", value );
	}

	public float VoiceLevel
	{
		get => Target.GetFloat( "voice" );
		set => Target.Set( "voice", value );
	}

	public bool IsSitting
	{
		get => Target.GetBool( "b_sit" );
		set => Target.Set( "b_sit", value );
	}

	public bool IsGrounded
	{
		get => Target.GetBool( "b_grounded" );
		set => Target.Set( "b_grounded", value );
	}

	public bool IsSwimming
	{
		get => Target.GetBool( "b_swim" );
		set => Target.Set( "b_swim", value );
	}

	public bool IsClimbing
	{
		get => Target.GetBool( "b_climbing" );
		set => Target.Set( "b_climbing", value );
	}

	public bool IsNoclipping
	{
		get => Target.GetBool( "b_noclip" );
		set => Target.Set( "b_noclip", value );
	}

	public bool IsWeaponLowered
	{
		get => Target.GetBool( "b_weapon_lower" );
		set => Target.Set( "b_weapon_lower", value );
	}

	public enum HoldTypes
	{
		None,
		Pistol,
		Rifle,
		Shotgun,
		HoldItem,
		Punch,
		Swing,
		RPG
	}

	public HoldTypes HoldType
	{
		get => (HoldTypes)Target.GetInt( "holdtype" );
		set => Target.Set( "holdtype", (int)value );
	}

	public enum Hand
	{
		Both,
		Right,
		Left
	}

	public Hand Handedness
	{
		get => (Hand)Target.GetInt( "holdtype_handedness" );
		set => Target.Set( "holdtype_handedness", (int)value );
	}

	public void TriggerJump()
	{
		Target.Set( "b_jump", true );
	}

	public void TriggerDeploy()
	{
		Target.Set( "b_deploy", true );
	}

	public enum MoveStyles
	{
		Auto,
		Walk,
		Run
	}

	/// <summary>
	/// We can force the model to walk or run, or let it decide based on the speed.
	/// </summary>
	public MoveStyles MoveStyle
	{
		get => (MoveStyles)Target.GetInt( "move_style" );
		set => Target.Set( "move_style", (int)value );
	}

	public enum SpecialMoveStyle
	{
		None,
		LedgeGrab,
		Roll,
		Slide
	}

	public SpecialMoveStyle SpecialMove
	{
		get => (SpecialMoveStyle)Target.GetInt( "special_movement_states" );
		set => Target.Set( "special_movement_states", (int)value );
	}

	public int FaceOverride
	{
		get => Target.GetInt( "face_override" );
		set => Target.Set( "face_override", value );
	}
}
using Sandbox;
using System.Collections.Generic;
using static Sandbox.ClothingContainer;

[Icon( "checkroom", "blue", "white" )]
[EditorHandle( "editor/citizenhead.png" )]
public sealed class ClothingFileDresser : Component
{
	// New struct to hold clothing and source together
	public struct ClothingSet
	{
		public List<Clothing> Clothes { get; set; }
		public SkinnedModelRenderer Source { get; set; }
		public bool IsHuman { get; set; }
	}

	[Property, InlineEditor] List<ClothingSet> Sets { get; set; } = new();

	[Button( "Dress" )]
	void DressCitizen()
	{
		foreach ( var set in Sets )
		{
			if ( set.Source == null || !set.Source.IsValid() )
				continue;

			var container = new ClothingContainer();
			container.PrefersHuman = set.IsHuman;
			container.Reset( set.Source );
			container.Clothing.Clear();

			foreach ( var clothing in set.Clothes )
			{
				if ( clothing is null )
					continue;

				var entry = new ClothingEntry( clothing );
				if ( container.Clothing.Contains( entry ) )
					continue;

				container.Clothing.Add( entry );
			}

			container.Normalize();
			container.Apply( set.Source );
		}
	}

	[Button( "UnDress" )]
	void UnDressCitizen()
	{
		foreach ( var set in Sets )
		{
			if ( set.Source == null || !set.Source.IsValid() )
				continue;

			var container = new ClothingContainer();
			container.Reset( set.Source );
			container.Clothing.Clear();
		}
	}
}
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace GeneralGame.Editor;

/// <summary>
/// Helper for building asset context menus with Create Material/Texture options.
/// </summary>
public static class AssetContextMenuHelper
{
    /// <summary>
    /// Add asset-type specific options like Create Material, Create Texture, etc.
    /// </summary>
    public static void AddAssetTypeOptions(Menu menu, Asset asset)
    {
        if (asset == null) return;

        var assetType = asset.AssetType;
        if (assetType == null) return;

        // Image files - can create Material, Texture, Sprite
        if (assetType == AssetType.ImageFile)
        {
            menu.AddSeparator();
            menu.AddOption("Create Material", "image", () => CreateMaterialFromImage(asset));
            menu.AddOption("Create Texture", "texture", () => CreateTextureFromImage(asset));
            menu.AddOption("Create Sprite", "emoji_emotions", () => CreateSpriteFromImage(asset));
        }

        // Shader files - can create Material
        if (assetType == AssetType.Shader)
        {
            menu.AddSeparator();
            menu.AddOption("Create Material", "image", () => CreateMaterialFromShader(asset));
        }

        // Mesh files (FBX, OBJ) - can create Model
        var meshExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".fbx", ".obj", ".dmx", ".gltf", ".glb" };
        if (meshExtensions.Contains(Path.GetExtension(asset.AbsolutePath)))
        {
            menu.AddSeparator();
            menu.AddOption("Create Model", "view_in_ar", () => CreateModelFromMesh(asset));
        }
    }

    private static void CreateTextureFromImage(Asset asset)
    {
        var assetName = asset.Name;

        var fd = new FileDialog(null);
        fd.Title = "Create Texture from Image..";
        fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
        fd.DefaultSuffix = ".vtex";
        fd.SelectFile($"{assetName}.vtex");
        fd.SetFindFile();
        fd.SetModeSave();
        fd.SetNameFilter("Texture File (*.vtex)");

        if (!fd.Execute())
            return;

        var imagePath = asset.RelativePath;

        // Create simple vtex JSON structure
        var vtexContent = new Dictionary<string, object>
        {
            { "Sequences", new object[]
                {
                    new Dictionary<string, object>
                    {
                        { "Source", imagePath },
                        { "IsLooping", true }
                    }
                }
            }
        };

        var json = Json.Serialize(vtexContent);
        File.WriteAllText(fd.SelectedFile, json);

        AssetSystem.RegisterFile(fd.SelectedFile);
    }

    private static void CreateMaterialFromImage(Asset asset)
    {
        string[] types = new[] { "color", "ao", "normal", "metallic", "rough", "diff", "diffuse", "nrm", "spec", "selfillum", "mask" };

        var assetName = asset.Name;

        foreach (var t in types)
        {
            if (assetName.EndsWith($"_{t}"))
                assetName = assetName.Substring(0, assetName.Length - (t.Length + 1));
        }

        var fd = new FileDialog(null);
        fd.Title = "Create Material from Image..";
        fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
        fd.DefaultSuffix = ".vmat";
        fd.SelectFile($"{assetName}.vmat");
        fd.SetFindFile();
        fd.SetModeSave();
        fd.SetNameFilter("Material File (*.vmat)");

        if (!fd.Execute())
            return;

        var assetPath = Path.GetDirectoryName(asset.AbsolutePath).NormalizeFilename(false);
        var assetPeers = AssetSystem.All
            .Where(x => x.AssetType == AssetType.ImageFile)
            .Where(x => x.AbsolutePath.StartsWith(assetPath))
            .ToArray();

        var assetPeersWithSameBaseName = assetPeers
            .Where(x => x.Name == assetName || x.Name.StartsWith(assetName + "_"))
            .ToArray();

        if (assetPeersWithSameBaseName.Length > 0)
        {
            assetPeers = assetPeersWithSameBaseName;
        }

        string texColor = assetPeers.Where(x => x.Name.Contains("_color") || x.Name.Contains("_diff")).Select(x => x.RelativePath).FirstOrDefault();
        texColor ??= asset.RelativePath;

        string texNormal = assetPeers.Where(x => x.Name.Contains("_nrm") || x.Name.Contains("_normal") || x.Name.Contains("_amb")).Select(x => x.RelativePath).FirstOrDefault() ?? "materials/default/default_normal.tga";
        string texAo = assetPeers.Where(x => x.Name.Contains("_ao") || x.Name.Contains("_occ") || x.Name.Contains("_amb")).Select(x => x.RelativePath).FirstOrDefault() ?? "materials/default/default_ao.tga";
        string texRough = assetPeers.Where(x => x.Name.Contains("_rough")).Select(x => x.RelativePath).FirstOrDefault() ?? "materials/default/default_rough.tga";

        string texMetallic = assetPeers.Where(x => x.Name.Contains("_metallic")).Select(x => x.RelativePath).FirstOrDefault();
        if (texMetallic != null)
        {
            texMetallic = $"\n\tF_METALNESS_TEXTURE 1\n\tF_SPECULAR 1\n\tTextureMetalness \"{texMetallic}\"";
        }

        string texSelfIllum = assetPeers.Where(x => x.Name.Contains("_selfillum")).Select(x => x.RelativePath).FirstOrDefault();
        if (texSelfIllum != null)
        {
            texSelfIllum = $"\n\tF_SELF_ILLUM 1\n\tTextureSelfIllumMask \"{texSelfIllum}\"";
        }

        string tintMask = assetPeers.Where(x => x.Name.Contains("_mask")).Select(x => x.RelativePath).FirstOrDefault();
        if (tintMask != null)
        {
            tintMask = $"\n\tF_TINT_MASK 1\n\tTextureTintMask \"{tintMask}\"";
        }

        var file = $@"
Layer0
{{
	shader ""shaders/complex.shader_c""

	TextureColor ""{texColor}""
	TextureAmbientOcclusion ""{texAo}""
	TextureNormal ""{texNormal}""
	TextureRoughness ""{texRough}""{texMetallic}{texSelfIllum}{tintMask}

}}
";
        File.WriteAllText(fd.SelectedFile, file);
        AssetSystem.RegisterFile(fd.SelectedFile);
    }

    private static async void CreateSpriteFromImage(Asset asset)
    {
        var assetName = asset.Name;

        var fd = new FileDialog(null);
        fd.Title = "Create Sprite from Image..";
        fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
        fd.DefaultSuffix = ".sprite";
        fd.SelectFile($"{assetName}.sprite");
        fd.SetFindFile();
        fd.SetModeSave();
        fd.SetNameFilter("Sprite File (*.sprite)");

        if (!fd.Execute())
            return;

        var path = Path.ChangeExtension(asset.Path, Path.GetExtension(asset.AbsolutePath));
        var sprite = Sprite.FromTexture(Texture.Load(path));
        var json = sprite.Serialize().ToJsonString();
        File.WriteAllText(fd.SelectedFile, json);

        var resultAsset = AssetSystem.RegisterFile(fd.SelectedFile);
        while (!resultAsset.IsCompiledAndUpToDate)
        {
            await Task.Delay(10);
        }
    }

    private static void CreateMaterialFromShader(Asset asset)
    {
        var assetName = asset.Name;

        var fd = new FileDialog(null);
        fd.Title = "Create Material from Shader..";
        fd.Directory = Path.GetDirectoryName(asset.AbsolutePath);
        fd.DefaultSuffix = ".vmat";
        fd.SelectFile($"{assetName}.vmat");
        fd.SetFindFile();
        fd.SetModeSave();
        fd.SetNameFilter("Material File (*.vmat)");

        if (!fd.Execute())
            return;

        var shaderPath = asset.GetCompiledFile();

        var file = $@"
Layer0
{{
	shader ""{shaderPath}""

}}
";
        File.WriteAllText(fd.SelectedFile, file);
        AssetSystem.RegisterFile(fd.SelectedFile);
    }

    private static void CreateModelFromMesh(Asset asset)
    {
        var targetPath = EditorUtility.SaveFileDialog("Create Model..", "vmdl", Path.ChangeExtension(asset.AbsolutePath, "vmdl"));
        if (targetPath == null)
            return;

        EditorUtility.CreateModelFromMeshFile(asset, targetPath);
    }
}
namespace Sandbox;

public readonly struct MeshSliceRegion
{
	public readonly float StartX { get; }
	public readonly float StartY { get; }
	public readonly float EndX { get; }
	public readonly float EndY { get; }

	public static MeshSliceRegion Full { get; } = new( 0.0f, 0.0f, 1.0f, 1.0f );

	public MeshSliceRegion( float startX, float startY, float endX, float endY )
	{
		StartX = startX;
		StartY = startY;
		EndX = endX;
		EndY = endY;
	}

	public Vector2 Map( Vector2 uv )
	{
		return Map( uv.x, uv.y );
	}

	public Vector2 Map( float x, float y )
	{
		return new Vector2(
			MapRange( x, 0.0f, 1.0f, StartX, EndX ),
			MapRange( y, 0.0f, 1.0f, StartY, EndY ) );
	}

	private static float MapRange( float value, float sourceMin, float sourceMax, float targetMin, float targetMax )
	{
		return (value - sourceMin) * (targetMax - targetMin) / (sourceMax - sourceMin) + targetMin;
	}
}
namespace Sandbox;

public sealed class MeshSliceResult
{
	public Model UpperModel { get; }
	public Model LowerModel { get; }

	internal MeshSliceResult( Model upperModel, Model lowerModel )
	{
		UpperModel = upperModel;
		LowerModel = lowerModel;
	}

	public GameObject CreateUpperHull( GameObject source, string name = "Upper_Hull" )
	{
		return CreateUpperHull( source, name, autoCopyPhysics: false );
	}

	public GameObject CreateUpperHull( GameObject source, string name = "Upper_Hull", bool autoCopyPhysics = false )
	{
		return CreateHullObject( source, UpperModel, name, autoCopyPhysics );
	}

	public GameObject CreateLowerHull( GameObject source, string name = "Lower_Hull" )
	{
		return CreateLowerHull( source, name, autoCopyPhysics: false );
	}

	public GameObject CreateLowerHull( GameObject source, string name = "Lower_Hull", bool autoCopyPhysics = false )
	{
		return CreateHullObject( source, LowerModel, name, autoCopyPhysics );
	}

	public GameObject[] CreateHulls( GameObject source, bool disableSource = false, bool autoCopyPhysics = false )
	{
		var upper = CreateUpperHull( source, autoCopyPhysics: autoCopyPhysics );
		var lower = CreateLowerHull( source, autoCopyPhysics: autoCopyPhysics );

		if ( disableSource && source is not null )
		{
			source.Enabled = false;
		}

		if ( upper is not null && lower is not null )
			return new[] { upper, lower };

		if ( upper is not null )
			return new[] { upper };

		if ( lower is not null )
			return new[] { lower };

		return null;
	}

	private static GameObject CreateHullObject( GameObject source, Model model, string name, bool autoCopyPhysics )
	{
		if ( source is null || model is null )
			return null;

		var newObject = new GameObject( source.Parent, source.Enabled, name );
		newObject.WorldTransform = source.WorldTransform;
		newObject.Tags.SetFrom( source.Tags );

		var sourceRenderer = source.GetComponent<ModelRenderer>( true );
		var renderer = newObject.GetOrAddComponent<ModelRenderer>();
		renderer.Model = model;

		if ( sourceRenderer is not null )
		{
			renderer.Tint = sourceRenderer.Tint;
			renderer.RenderType = sourceRenderer.RenderType;
		}

		if ( autoCopyPhysics )
		{
			CopyPhysicsFromSource( source, newObject, model );
		}

		return newObject;
	}

	private static void CopyPhysicsFromSource( GameObject source, GameObject destination, Model hullModel )
	{
		var sourceBody = source.GetComponent<Rigidbody>( true );
		if ( sourceBody is not null )
		{
			var destinationBody = destination.AddComponent<Rigidbody>( false );
			CopyRigidbody( sourceBody, destinationBody );
			destinationBody.Enabled = sourceBody.Enabled;
		}

		Collider sourceColliderTemplate = null;
		foreach ( var sourceCollider in source.GetComponents<Collider>( true ) )
		{
			sourceColliderTemplate = sourceCollider;
			break;
		}

		if ( sourceColliderTemplate is not null && hullModel is not null )
		{
			var destinationCollider = destination.AddComponent<ModelCollider>( false );
			CopyCollider( sourceColliderTemplate, destinationCollider );
			destinationCollider.Model = hullModel;
			destinationCollider.Enabled = sourceColliderTemplate.Enabled;
		}
		else if ( sourceBody is not null && hullModel is not null )
		{
			var fallbackCollider = destination.AddComponent<ModelCollider>( false );
			fallbackCollider.Model = hullModel;
			fallbackCollider.Enabled = sourceBody.Enabled;
		}
	}

	private static void CopyCollider( Collider source, Collider destination )
	{
		destination.Static = source.Static;
		destination.IsTrigger = source.IsTrigger;
		destination.Surface = source.Surface;
		destination.SurfaceVelocity = source.SurfaceVelocity;
		destination.Friction = source.Friction;
		destination.Elasticity = source.Elasticity;
		destination.RollingResistance = source.RollingResistance;
		destination.ColliderFlags = source.ColliderFlags;
	}

	private static void CopyRigidbody( Rigidbody source, Rigidbody destination )
	{
		destination.Gravity = source.Gravity;
		destination.GravityScale = source.GravityScale;
		destination.LinearDamping = source.LinearDamping;
		destination.AngularDamping = source.AngularDamping;
		destination.MassOverride = source.MassOverride;
		destination.OverrideMassCenter = source.OverrideMassCenter;
		destination.MassCenterOverride = source.MassCenterOverride;
		destination.Locking = source.Locking;
		destination.StartAsleep = source.StartAsleep;
		destination.RigidbodyFlags = source.RigidbodyFlags;
		destination.EnableImpactDamage = source.EnableImpactDamage;
		destination.MinImpactDamageSpeed = source.MinImpactDamageSpeed;
		destination.ImpactDamage = source.ImpactDamage;
		destination.MotionEnabled = source.MotionEnabled;
		destination.CollisionEventsEnabled = source.CollisionEventsEnabled;
		destination.CollisionUpdateEventsEnabled = source.CollisionUpdateEventsEnabled;
		destination.EnhancedCcd = source.EnhancedCcd;
		destination.Velocity = source.Velocity;
		destination.AngularVelocity = source.AngularVelocity;
	}
}
using Sandbox.Sboku;
using Sandbox.Sboku.Shared;

namespace Sandbox.AI.Default;
internal class ReloadState : StateBase, ICombatState
{
    public ReloadState(SbokuBase bot) : base(bot)
    {
    }
    public override void OnSet()
    {
        Bot.IsReloading = true;
    }

    public override void OnUnset()
    {
    }

    public void OnReloadFinish()
    {
        Bot.SetCombatState<ShootState>();
        Bot.IsReloading = false;
    }
}
using Sandbox.Sboku;
using Sandbox.Sboku.Shared;

namespace Sandbox.AI.Default;
public class SbokuParent
{
    protected SbokuBase Bot { get; }
    protected Scene Scene => Bot.Scene;
    protected SbokuSettings Settings => Bot.Settings;
    protected ISbokuTarget Target => Bot.Target;
    protected ISbokuWeapon Weapon => Bot.Weapon;

    /// <summary>
    /// Get squared distance to target. If turget is null, we'll get NRE.
    /// </summary>
    protected float SquaredDistanceToTarget => Bot.WorldPosition.DistanceSquared(Target.GameObject.WorldPosition);

    protected SbokuParent(SbokuBase bot)
    {
        Bot = bot;
    }
}
using Sandbox.Sboku;

namespace Sandbox.AI.Default;
internal class ShootState : StateBase, ICombatState
{
    private Burst burst;
    public ShootState(SbokuBase bot) : base(bot)
    {
    }

    private record Burst
    {
        public float Period;
        public TimeSince Timer = new TimeSince();
        public Burst(float period)
        {
            Period = period;
            Timer = 0;
        }
        public bool CanFire()
            => Timer < Period;
        public bool ShouldStop()
            => Timer > Period;
        public bool CanContinue()
            => Timer > Period * 2;
    }

    public override void Think()
    {
        if (Weapon.HasAmmo())
        {
            if (burst?.CanFire() ?? true)
            {
                Bot.IsShooting = Scene.Trace.Ray(Bot.EyePos, Target.GameObject.WorldPosition + Bot.HeightToAimAt)
                                            .IgnoreGameObjectHierarchy(Bot.GameObject)
                                            .Run().GameObject?.Parent == Target.GameObject;
            }
            else if (burst != null)
            {
                if (burst.CanContinue())
                    burst = null;
                else if (burst.ShouldStop())
                    Bot.IsShooting = false;
            }
            
            if (Bot.IsShooting && burst == null)
            {
                burst = new(Bot.BurstPeriod);
            }
        }
        else
        {
            OnReload();
        }
    }

    public override void OnUnset()
    {
        Bot.IsShooting = false;
    }

    public void OnReload()
    {
        lock (this)
        {
            Bot.IsShooting = false;
            Bot.SetCombatState<ReloadState>();
        }
    }
}
using Sandbox.Navigation;
using System;
using System.Collections.Generic;

namespace Sandbox.Sboku;
internal static class Extensions
{
    // Sometimes I get `Failed to compare two elements in the array.` for some reason
    public static List<Vector3> GetSimplePathSafe(this NavMesh mesh, Vector3 from, Vector3 to)
    {
        try
        {
            return mesh.GetSimplePath(from, to);
        }
        catch (Exception e)
        {
            var ex = new Exception("NavMesh fail: " + e.Message, e.InnerException);
            Log.Error(ex);
            return new();
        }
    }
}
namespace Sandbox.Shared;
public interface ISbokuCondition
{
    bool If();
    void Then();
    /// <summary>
    /// Should we stop evaluating other conditions if the condition is true
    /// </summary>
    /// <returns></returns>
    bool IsTerminal();
}
using System;
using System.Collections.Generic;
using Sandbox.Sboku;
using Sandbox.Shared;

namespace Sandbox.AI.Default;
public class Conditions
{
    private abstract class SimpleCondition : SbokuParent, ISbokuCondition
    {
        public SimpleCondition(SbokuBase bot) : base(bot)
        {
        }

        public abstract bool If();
        public abstract void Then();

        public bool IsTerminal()
            => false;

    }
    private class StopCondion : SimpleCondition
    {
        public StopCondion(SbokuBase bot) : base(bot)
        {
        }
        public override bool If()
                => !(Bot.IsActiveActionState<IdleActionState>() && Bot.IsActiveCombatState<IdleCombatState>())
                   && (Weapon == null
                   ||  Target == null
                   || !Target.IsValid
                   || !Target.IsAlive
                   || SquaredDistanceToTarget > MathF.Pow(Bot.SearchRange, 2));
        public override void Then()
            => Bot.ResetState();
    }
    private class ChaseCondition : SimpleCondition
    {
        public ChaseCondition(SbokuBase bot) : base(bot)
        {
        }
        public override bool If()
                => Bot.Target != null && SquaredDistanceToTarget > MathF.Pow(Bot.MaxFightRange, 2);
        public override void Then()
            => Bot.SetActionState<ChaseState>();
    }


    public static List<ISbokuCondition> Get(SbokuBase bot) =>
        new List<ISbokuCondition>()
        {
            new StopCondion(bot),
            new ChaseCondition(bot)
        };
}
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.Json.Nodes;
using Sandbox;

namespace SpriteTools;

public partial class SpriteResource
{
    public override int ResourceVersion => 1;

    [JsonUpgrader(typeof(SpriteResource), 1)]
    static void Upgrader_v1(JsonObject json)
    {
        if (json.ContainsKey("Looping"))
        {
            var wasLooping = json["Looping"].GetValue<bool>();
            json["LoopMode"] = (int)(wasLooping ? SpriteResource.LoopMode.Forward : SpriteResource.LoopMode.None);
        }
    }
}
using Editor;
using Sandbox;
using System;
using System.Linq;
using System.Reflection;

namespace SpriteTools.TilesetTool;

[Inspector( typeof( TilesetTool ) )]
public class TilesetToolInspector : InspectorWidget
{
	public static TilesetToolInspector Active { get; private set; }
	internal TilesetTool Tool;
	StatusWidget Header;

	ScrollArea scrollArea;
	ControlSheet toolSheet;
	ControlSheet mainSheet;
	ControlSheet selectedSheet;

	public TilesetToolInspector ( SerializedObject so ) : base( so )
	{
		if ( so.Targets.FirstOrDefault() is not TilesetTool tool ) return;

		Tool = tool;
		// Tool.UpdateInspector += UpdateHeader;
		// Tool.UpdateInspector += UpdateSelectedSheet;

		Layout = Layout.Column();
		Layout.Margin = 4;
		Layout.Spacing = 8;

		Active = this;
		Rebuild();
	}

	int lastBuildHash = 0;
	[EditorEvent.Frame]
	void Frame ()
	{
		int buildHash = 0;
		if ( Tool.SelectedComponent.IsValid() )
		{
			buildHash += Tool.SelectedComponent.Layers.IndexOf( Tool?.SelectedLayer );
			buildHash += Tool?.SelectedLayer?.TilesetResource?.ResourceId ?? 0;
		}
		if ( buildHash != lastBuildHash )
		{
			lastBuildHash = buildHash;
			Rebuild();
		}
	}

	[EditorEvent.Hotload]
	void Rebuild ()
	{
		if ( Layout is null ) return;
		Layout.Clear( true );

		scrollArea = new ScrollArea( this );
		scrollArea.Canvas = new Widget();
		scrollArea.Canvas.Layout = Layout.Column();
		scrollArea.Canvas.VerticalSizeMode = SizeMode.CanGrow;
		scrollArea.Canvas.HorizontalSizeMode = SizeMode.Flexible;
		scrollArea.Canvas.Layout.Spacing = 8;
		Layout.Add( scrollArea );

		Header = new StatusWidget( this );
		scrollArea.Canvas.Layout.Add( Header );
		UpdateHeader();

		mainSheet = new ControlSheet();
		scrollArea.Canvas.Layout.Add( mainSheet );
		UpdateMainSheet();

		selectedSheet = null;
		UpdateSelectedSheet();

		toolSheet = new ControlSheet();
		scrollArea.Canvas.Layout.Add( toolSheet );
		UpdateToolSheet();

		// Preview = new Preview.Preview(this);
		// scrollArea.Canvas.Layout.Add(Preview);

		scrollArea.Canvas.Layout.AddStretchCell();

	}

	internal void UpdateHeader ()
	{
		Header.Text = "Paint Tiles";
		Header.Color = ( false ) ? Theme.Red : Theme.Blue;
		Header.Icon = ( false ) ? "warning" : "dashboard";
		Header.Update();
	}

	internal void UpdateToolSheet ()
	{
		if ( !( Layout?.IsValid ?? false ) ) return;
		if ( toolSheet is null ) return;

		toolSheet?.Clear( true );

		if ( Tool?.Settings is not null )
		{
			toolSheet.AddObject( Tool.Settings.GetSerialized(), x =>
			{
				return x.HasAttribute<PropertyAttribute>() && x.PropertyType != typeof( Action );
			} );
		}
	}

	internal void UpdateMainSheet ()
	{
		if ( !( Layout?.IsValid ?? false ) ) return;
		if ( mainSheet is null ) return;

		mainSheet?.Clear( true );

		if ( Tool?.CurrentTool is not null )
		{
			var toolName = ( Tool.CurrentTool.GetType()?.GetCustomAttribute<TitleAttribute>()?.Value ?? "Unknown" ) + " Tool";
			mainSheet.AddObject( Tool.CurrentTool.GetSerialized(), x => x.HasAttribute<PropertyAttribute>() && x.PropertyType != typeof( Action ) );
		}
		if ( Tool.SelectedComponent.IsValid() )
		{
			mainSheet.AddObject( Tool.SelectedComponent.GetSerialized(), x =>
			{
				if ( x.Name == nameof( TilesetComponent.Layers ) ) return true;
				if ( !x.HasAttribute<PropertyAttribute>() ) return false;
				if ( x.TryGetAttribute<FeatureAttribute>( out var feature ) && feature.Title == "Collision" ) return false;
				if ( x.PropertyType == typeof( Action ) ) return false;
				if ( x.PropertyType == typeof( TilesetComponent.ComponentControls ) ) return false;

				return true;
			} );
		}
	}

	internal void UpdateSelectedSheet ()
	{
		if ( !( Layout?.IsValid ?? false ) ) return;

		if ( selectedSheet is null || !( selectedSheet?.IsValid ?? false ) )
		{
			selectedSheet = new ControlSheet();
			scrollArea.Canvas.Layout.Add( selectedSheet );
		}

		selectedSheet?.Clear( true );
		if ( Tool.SelectedLayer is not null )
		{
			selectedSheet.AddObject( Tool.SelectedLayer.GetSerialized(), x => x.HasAttribute<PropertyAttribute>() && x.PropertyType != typeof( Action ) );
		}
	}

	private class StatusWidget : Widget
	{
		public string Icon { get; set; }
		public string Text { get; set; }
		public string LeadText { get; set; }
		public Color Color { get; set; }

		TilesetToolInspector Inspector;

		public StatusWidget ( TilesetToolInspector parent ) : base( parent )
		{
			Inspector = parent;
			MinimumSize = 48;
			Cursor = CursorShape.Finger;
			SetSizeMode( SizeMode.Default, SizeMode.CanShrink );
		}

		protected override void OnPaint ()
		{
			var rect = new Rect( 0, Size );

			Paint.ClearPen();
			Paint.SetBrush( Theme.WindowBackground.Lighten( 0.9f ) );
			Paint.DrawRect( rect );

			rect.Left += 8;

			Paint.SetPen( Color );
			var iconRect = Paint.DrawIcon( rect, Icon, 24, TextFlag.LeftCenter );

			rect.Top += 8;
			rect.Left = iconRect.Right + 8;

			Paint.SetPen( Color );
			Paint.SetDefaultFont( 10, 500 );
			var titleRect = Paint.DrawText( rect, Text, TextFlag.LeftTop );

			rect.Top = titleRect.Bottom + 2;

			Paint.SetPen( Color.WithAlpha( 0.6f ) );
			Paint.SetDefaultFont( 8, 400 );
			var preText = "Selected Component:";
			if ( !Inspector.Tool.SelectedComponent.IsValid() )
				preText = "No Tileset Component";
			var selectedRect = Paint.DrawText( rect, preText, TextFlag.LeftTop );
			if ( Inspector.Tool.SelectedComponent.IsValid() )
			{
				var name = Inspector.Tool.SelectedComponent.GameObject.Name;
				var textPos = selectedRect.TopRight + new Vector2( 8, 0 );
				var textRect = new Rect( textPos, Paint.MeasureText( name ) );
				var boxRect = textRect.Grow( 4, 2, 18, 2 );
				var isHovering = Paint.HasMouseOver;
				var boxCol = isHovering ? Theme.ControlBackground.Lighten( 0.3f ) : Theme.ControlBackground.Darken( 0.2f );
				var color = isHovering ? Color.Lighten( 0.2f ) : Color;
				Paint.SetBrushAndPen( boxCol, Color.Transparent );
				Paint.DrawRect( boxRect );
				Paint.SetPen( color );
				var drawnRect = Paint.DrawText( textPos, name );
				var iconPos = drawnRect.TopRight + new Vector2( 2, 0 );
				Paint.DrawIcon( Rect.FromPoints( iconPos, iconPos + 14 ), "expand_more", 14 );

			}
		}

		protected override void OnMouseClick ( MouseEvent e )
		{
			base.OnMouseClick( e );

			var components = SceneEditorSession.Active.Scene.GetAllComponents<TilesetComponent>();
			Log.Info( components.Count() );
			if ( components.Count() == 0 ) return;

			var menu = new Menu();

			foreach ( var tileset in components )
			{
				var option = menu.AddOption( tileset.GameObject.Name, null, () =>
				{
					Inspector.Tool.SelectedComponent = tileset;
					Inspector.Tool.SelectedLayer = tileset.Layers.FirstOrDefault();
				} );
				option.Checkable = true;
				option.Checked = tileset == Inspector.Tool.SelectedComponent;
			}

			menu.OpenAtCursor();
		}
	}
}
using Sandbox;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

namespace SpriteTools;

[Category( "2D" )]
[Title( "2D Tileset" )]
[Icon( "calendar_view_month" )]
[Tint( EditorTint.Yellow )]
public partial class TilesetComponent : Component, Component.ExecuteInEditor
{
	/// <summary>
	/// The Layers within the TilesetComponent
	/// </summary>
	[Property, Group( "Layers" )]
	public List<Layer> Layers
	{
		get => _layers;
		set
		{
			_layers = value;
			foreach ( var layer in _layers )
			{
				layer.TilesetComponent = this;
			}
		}
	}
	List<Layer> _layers;

	[Property, WideMode( HasLabel = false )]
	ComponentControls InternalControls { get; set; }

	/// <summary>
	/// Whether or not the component should generate a collider based on the specified Collision Layer
	/// </summary>
	[Property, FeatureEnabled( "Collision" )]
	public bool HasCollider
	{
		get => _hasCollider;
		set
		{
			if ( value == _hasCollider ) return;
			_hasCollider = value;
			if ( value ) CreateCollider();
			else DestroyCollider();
		}
	}
	bool _hasCollider;

	/// <inheritdoc cref="Collider.Static" />
	[Property, Feature( "Collision" )]
	public bool Static
	{
		get => _static;
		set
		{
			if ( value == _static ) return;
			_static = value;
			if ( Collider.IsValid() ) Collider.Static = value;
		}
	}
	private bool _static = true;

	/// <inheritdoc cref="Collider.IsTrigger" />
	[Property, Feature( "Collision" )]
	public bool IsTrigger
	{
		get => _isTrigger;
		set
		{
			if ( value == _isTrigger ) return;
			_isTrigger = value;
			if ( Collider.IsValid() ) Collider.IsTrigger = value;
		}
	}
	private bool _isTrigger = false;

	/// <summary>
	/// The width of the generated collider
	/// </summary>
	[Property, Feature( "Collision" )]
	public float ColliderWidth
	{
		get => _colliderWidth;
		set
		{
			if ( value < 0f ) _colliderWidth = 0f;
			else if ( value == _colliderWidth ) return;
			_colliderWidth = value;
			Collider?.RebuildMesh();
		}
	}
	float _colliderWidth;

	/// <inheritdoc cref="Collider.Friction" />
	[Property, Feature( "Collision" ), Group( "Surface Properties" )]
	[Range( 0f, 1f, true, true ), Step( 0.01f )]
	public float? Friction
	{
		get => _friction;
		set
		{
			if ( value == _friction ) return;
			_friction = value;
			if ( Collider.IsValid() ) Collider.Friction = value;
		}
	}
	private float? _friction;

	/// <inheritdoc cref="Collider.Surface" />
	[Property, Feature( "Collision" ), Group( "Surface Properties" )]
	public Surface Surface
	{
		get => _surface;
		set
		{
			if ( value == _surface ) return;
			_surface = value;
			if ( Collider.IsValid() ) Collider.Surface = value;
		}
	}
	private Surface _surface;

	/// <inheritdoc cref="Collider.SurfaceVelocity" />
	[Property, Feature( "Collision" ), Group( "Surface Properties" )]
	public Vector3 SurfaceVelocity
	{
		get => _surfaceVelocity;
		set
		{
			if ( value == _surfaceVelocity ) return;
			_surfaceVelocity = value;
			if ( Collider.IsValid() ) Collider.SurfaceVelocity = value;
		}
	}
	private Vector3 _surfaceVelocity;

	[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
	public Action<Collider> OnTriggerEnter { get; set; }

	[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
	public Action<Collider> OnTriggerExit { get; set; }

	/// <summary>
	/// Whether or not the associated Collider is dirty. Setting this to true will rebuild the Collider on the next frame.
	/// </summary>
	public bool IsDirty
	{
		get => Collider?.IsDirty ?? false;
		set
		{
			if ( !Collider.IsValid() ) return;
			Collider.IsDirty = value;
		}
	}
	TilesetCollider Collider;
	internal List<TilesetSceneObject> _sos = new();

	protected override void OnEnabled ()
	{
		base.OnEnabled();

		CreateCollider();

		if ( Layers is null ) return;
		foreach ( var layer in Layers )
		{
			layer.TilesetComponent = this;
		}
	}

	protected override void OnDisabled ()
	{
		base.OnDisabled();

		DestroyCollider();

		foreach ( var _so in _sos )
		{
			_so.Delete();
		}
		_sos.Clear();
	}

	protected override void OnUpdate ()
	{
		base.OnUpdate();

		_sos ??= new();
		Layers ??= new();
		var _newSos = new List<TilesetSceneObject>();
		foreach ( var sos in _sos )
		{
			if ( sos is not null || sos.IsValid() )
			{
				_newSos.Add( sos );
			}
			else
			{
				sos?.Delete();
			}
		}
		_sos = _newSos;
		if ( Layers.Count != _sos.Count )
		{
			RebuildSceneObjects();
		}
	}

	protected override void OnTagsChanged ()
	{
		base.OnTagsChanged();

		foreach ( var _so in _sos )
			_so?.Tags.SetFrom( Tags );
	}

	protected override void OnPreRender ()
	{
		base.OnPreRender();

		if ( Layers is null ) return;
		if ( Layers.Count == 0 )
		{
			return;
		}

		foreach ( var _so in _sos )
		{
			if ( !_so.IsValid() ) continue;
			_so.RenderingEnabled = true;
			_so.Transform = Transform.World;
			_so.Flags.CastShadows = false;
			_so.Flags.IsOpaque = false;
			_so.Flags.IsTranslucent = true;
		}
	}

	protected override void DrawGizmos ()
	{
		base.DrawGizmos();

		var bounds = GetBounds();
		Gizmo.Hitbox.BBox( bounds );

		if ( !Gizmo.IsSelected ) return;

		using ( Gizmo.Scope( "tileset", new Transform( 0, WorldRotation.Inverse, 1 ) ) )
		{
			Gizmo.Draw.Color = Color.Yellow;
			Gizmo.Draw.LineThickness = 1f;
			Gizmo.Draw.LineBBox( bounds );
		}
	}

	public BBox GetBounds ()
	{
		var bounds = BBox.FromPositionAndSize( 0, 0 );
		foreach ( var _so in _sos )
		{
			if ( !_so.IsValid() ) continue;

			var boundSize = _so.Bounds.Size;
			if ( ( boundSize.x + boundSize.y + boundSize.z ) > ( bounds.Size.x + bounds.Size.y + bounds.Size.z ) )
			{
				bounds = _so.Bounds.Translate( -_so.Position );
			}
		}

		return bounds;
	}

	void RebuildSceneObjects ()
	{
		foreach ( var _so in _sos )
		{
			_so.Delete();
		}

		_sos = new List<TilesetSceneObject>();
		for ( int i = 0; i < Layers.Count; i++ )
		{
			_sos.Add( new TilesetSceneObject( this, Scene.SceneWorld, i ) );
		}
	}

	void CreateCollider ()
	{
		if ( !HasCollider ) return;
		if ( Collider.IsValid() ) return;
		Collider = AddComponent<TilesetCollider>();
		Collider.Flags |= ComponentFlags.Hidden | ComponentFlags.NotSaved;
		Collider.Tileset = this;
		Collider.Static = Static;
		Collider.IsTrigger = IsTrigger;
		Collider.Friction = Friction;
		Collider.Surface = Surface;
		Collider.SurfaceVelocity = SurfaceVelocity;
		Collider.OnTriggerEnter += OnTriggerEnter;
		Collider.OnTriggerExit += OnTriggerExit;
	}

	void DestroyCollider ()
	{
		if ( Collider.IsValid() )
			Collider.Destroy();
		Collider = null;
	}

	/// <summary>
	/// Returns the Layer with the specified name
	/// </summary>
	/// <param name="name"></param>
	/// <returns></returns>
	public Layer GetLayerFromName ( string name )
	{
		return Layers.FirstOrDefault( x => x.Name == name );
	}

	/// <summary>
	/// Returns the Layer at the specified index
	/// </summary>
	/// <param name="index"></param>
	/// <returns></returns>
	public Layer GetLayerFromIndex ( int index )
	{
		if ( index < 0 || index >= Layers.Count ) return null;
		return Layers[index];
	}

	public class Layer
	{
		/// <summary>
		/// The name of the Layer
		/// </summary>
		public string Name { get; set; }

		/// <summary>
		/// Whether or not this Layer is currently being rendered
		/// </summary>
		public bool IsVisible { get; set; }

		/// <summary>
		/// Whether or not this Layer is locked. Locked Layers will ignore any attempted changes
		/// </summary>
		public bool IsLocked { get; set; }

		/// <summary>
		/// The Tileset that this Layer uses
		/// </summary>
		[Property, Group( "Selected Layer" )] public TilesetResource TilesetResource { get; set; }

		/// <summary>
		/// The height of the Layer
		/// </summary>
		[Property, Group( "Selected Layer" )] public float? Height { get; set; } = null;

		/// <summary>
		/// Whether or not this Layer dictates the collision mesh
		/// </summary>
		[Group( "Selected Layer" ), Title( "Has Collisions" )] public bool IsCollisionLayer { get; set; }

		/// <summary>
		/// A dictionary of all Tiles in the layer by their position.
		/// </summary>
		public Dictionary<Vector2Int, Tile> Tiles { get; set; }

		/// <summary>
		/// A dictionary containing a list of positions for each Autotile Brush by their ID.
		/// </summary>
		public Dictionary<Guid, List<AutotilePosition>> Autotiles { get; set; }

		/// <summary>
		/// The TilesetComponent that this Layer belongs to
		/// </summary>
		[JsonIgnore, Hide] public TilesetComponent TilesetComponent { get; set; }

		public Layer ( string name = "Untitled Layer" )
		{
			Name = name;
			IsVisible = true;
			IsLocked = false;
			Tiles = new();
		}

		/// <summary>
		/// Returns an exact copy of the Layer
		/// </summary>
		/// <returns></returns>
		public Layer Copy ()
		{
			var layer = new Layer( Name )
			{
				IsVisible = IsVisible,
				IsLocked = IsLocked,
				Tiles = new(),
				IsCollisionLayer = false,
				TilesetComponent = TilesetComponent,
			};

			foreach ( var tile in Tiles )
			{
				layer.Tiles[tile.Key] = tile.Value.Copy();
			}

			return layer;
		}

		/// <summary>
		/// Set a tile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="position"></param>
		/// <param name="tileId"></param>
		/// <param name="cellPosition"></param>
		/// <param name="angle"></param>
		/// <param name="flipX"></param>
		/// <param name="flipY"></param>
		/// <param name="rebuild"></param>
		public void SetTile ( Vector2Int position, Guid tileId, Vector2Int cellPosition = default, int angle = 0, bool flipX = false, bool flipY = false, bool rebuild = true, bool removeAutotile = true )
		{
			if ( IsLocked ) return;
			var tile = new Tile( tileId, cellPosition, angle, flipX, flipY );
			Tiles[position] = tile;
			if ( rebuild && TilesetComponent.IsValid() )
				TilesetComponent.IsDirty = true;

			if ( removeAutotile && Autotiles is not null )
			{
				foreach ( var group in Autotiles )
				{
					foreach ( var autotile in group.Value )
					{
						if ( autotile.Position == position )
						{
							Autotiles[group.Key].Remove( autotile );
							break;
						}
					}
				}
			}
		}

		/// <summary>
		/// Get the Tile at the specified position
		/// </summary>
		/// <param name="position"></param>
		/// <returns></returns>
		public Tile GetTile ( Vector2Int position )
		{
			return Tiles[position];
		}

		/// <summary>
		/// Get the Tile at the specified position
		/// </summary>
		/// <param name="position"></param>
		/// <returns></returns>
		public Tile GetTile ( Vector3 position )
		{
			return Tiles[new Vector2Int( (int)position.x, (int)position.y )];
		}

		/// <summary>
		/// Remove the Tile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="position"></param>
		public void RemoveTile ( Vector2Int position )
		{
			if ( IsLocked ) return;
			Tiles.Remove( position );

			if ( Autotiles is not null )
			{
				foreach ( var group in Autotiles )
				{
					foreach ( var autotile in group.Value )
					{
						if ( autotile.Position == position )
						{
							Autotiles[group.Key].Remove( autotile );
							break;
						}
					}
				}
			}
		}

		/// <summary>
		/// Set an Autotile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="autotileBrush"></param>
		/// <param name="position"></param>
		/// <param name="enabled"></param>
		///	<param name="update"></param>
		/// <param name="isMerging"></param>
		public void SetAutotile ( AutotileBrush autotileBrush, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
		{
			SetAutotile( autotileBrush.Id, position, enabled, update, isMerging );
		}

		/// <summary>
		/// Set an Autotile at the specified position. Will fail if IsLocked is true.
		/// </summary>
		/// <param name="autotileId"></param>
		/// <param name="position"></param>
		/// <param name="enabled"></param>
		/// <param name="update"></param>
		/// <param name="isMerging"></param>
		public void SetAutotile ( Guid autotileId, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
		{
			if ( IsLocked ) return;
			Autotiles ??= new();

			foreach ( var group in Autotiles )
			{
				if ( group.Key == autotileId ) continue;
				foreach ( var autotile in group.Value )
				{
					if ( autotile.Position == position )
					{
						Autotiles[group.Key].Remove( autotile );
						break;
					}
				}
			}

			if ( !Autotiles.ContainsKey( autotileId ) )
				Autotiles[autotileId] = new List<AutotilePosition>();

			bool shouldUpdate = false;
			if ( enabled )
			{
				if ( !Autotiles[autotileId].Any( x => x.Position == position ) )
				{
					Autotiles[autotileId].Add( new( position, isMerging ) );
					shouldUpdate = true;
				}
			}
			else
			{
				var foundPos = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
				if ( foundPos is not null )
				{
					Tiles.Remove( position );
					Autotiles[autotileId].Remove( foundPos );
					shouldUpdate = true;
				}
				else
				{
					RemoveTile( position );
				}
			}

			if ( update && shouldUpdate )
			{
				UpdateAutotile( autotileId, position, !enabled, shouldMerge: isMerging );
			}
		}

		/// <summary>
		/// Update the Autotile at the specified position. Used when manually modifying the placed autotiles.
		/// </summary>
		/// <param name="autotileId"></param>
		/// <param name="position"></param>
		/// <param name="checkErased"></param>
		/// <param name="updateSurrounding"></param>
		/// <param name="shouldMerge"></param>
		public void UpdateAutotile ( Guid autotileId, Vector2Int position, bool checkErased, bool updateSurrounding = true, bool shouldMerge = false )
		{
			if ( !Autotiles.ContainsKey( autotileId ) ) return;

			var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
			var autotile = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
			if ( autotile is not null )
			{
				if ( shouldMerge ) autotile.ShouldMerge = true;
				if ( autotile.ShouldMerge ) shouldMerge = true;

				var bitmask = GetAutotileBitmask( autotileId, position, shouldMerge );
				if ( bitmask == -1 )
				{
					if ( checkErased ) RemoveTile( position );
				}
				else
				{
					if ( brush is not null )
					{
						var tile = brush.GetTileFromBitmask( bitmask );
						if ( tile is not null )
						{
							SetTile( position, tile.Id, Vector2Int.Zero, 0, false, false, false, removeAutotile: false );
						}
						else
						{
							Log.Warning( $"Tile not found for bitmask {bitmask} in AutotileBrush {brush.Name}" );
						}
					}
				}
			}

			if ( updateSurrounding )
			{
				var up = position.WithY( position.y + 1 );
				var down = position.WithY( position.y - 1 );
				var left = position.WithX( position.x - 1 );
				var right = position.WithX( position.x + 1 );
				var upLeft = up.WithX( left.x );
				var upRight = up.WithX( right.x );
				var downLeft = down.WithX( left.x );
				var downRight = down.WithX( right.x );

				if ( brush is not null && brush.AutotileType == AutotileType.Bitmask2x2Edge )
				{
					ClearInvalidAutotile( autotileId, up );
					ClearInvalidAutotile( autotileId, down );
					ClearInvalidAutotile( autotileId, left );
					ClearInvalidAutotile( autotileId, right );
					ClearInvalidAutotile( autotileId, upLeft );
					ClearInvalidAutotile( autotileId, upRight );
					ClearInvalidAutotile( autotileId, downLeft );
					ClearInvalidAutotile( autotileId, downRight );
				}

				UpdateAutotile( autotileId, up, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, down, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, left, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, right, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, upLeft, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, upRight, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, downLeft, checkErased, false, shouldMerge );
				UpdateAutotile( autotileId, downRight, checkErased, false, shouldMerge );
			}
		}

		void ClearInvalidAutotile ( Guid autotileId, Vector2Int position )
		{
			if ( !Tiles.TryGetValue( position, out var tile ) ) return;

			var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );

			if ( brush is null ) return;
			if ( brush.AutotileType != AutotileType.Bitmask2x2Edge ) return;
			if ( !brush.Tiles.Any( x => x.Tiles.Any( y => y.Id == tile.TileId ) ) ) return;
			if ( GetAutotileBitmask( autotileId, position ) != -1 ) return;

			RemoveTile( position );
		}


		public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, bool mergeAll = false )
		{
			if ( Autotiles is null || ( !mergeAll && !Autotiles.ContainsKey( autotileId ) ) ) return -1;

			List<AutotilePosition> positions = new();
			if ( mergeAll )
			{
				foreach ( var kvp in Autotiles )
				{
					positions.AddRange( kvp.Value );
				}
			}
			else
			{
				positions = Autotiles[autotileId];
			}
			int value = 0;

			var up = position.WithY( position.y + 1 );
			var down = position.WithY( position.y - 1 );
			var left = position.WithX( position.x - 1 );
			var right = position.WithX( position.x + 1 );

			var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
			if ( brush is null ) return 0;

			bool is2x2 = brush.AutotileType == AutotileType.Bitmask2x2Edge;
			if ( is2x2 )
			{
				foreach ( var pos in positions )
				{
					if ( pos.Position == up ) value += 1;
					if ( pos.Position == left ) value += 2;
					if ( pos.Position == right ) value += 4;
					if ( pos.Position == down ) value += 8;
				}
				switch ( value )
				{
					case 0:
					case 1:
					case 2:
					case 4:
					case 8:
					case 9:
					case 6:
						return -1;
				}
				value = 0;
			}

			var upLeft = up.WithX( left.x );
			var upRight = up.WithX( right.x );
			var downLeft = down.WithX( left.x );
			var downRight = down.WithX( right.x );

			foreach ( var thing in positions )
			{
				var pos = thing.Position;
				if ( pos == upLeft ) value += 1;
				if ( pos == up ) value += 2;
				if ( pos == upRight ) value += 4;
				if ( pos == left ) value += 8;
				if ( pos == right ) value += 16;
				if ( pos == downLeft ) value += 32;
				if ( pos == down ) value += 64;
				if ( pos == downRight ) value += 128;
			}

			if ( is2x2 )
			{
				switch ( value )
				{
					case 46:
					case 116:
					case 147:
					case 201:
						return -1;
				}
			}

			return value;
		}

		public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, Dictionary<Vector2Int, bool> overrides, bool mergeAll = false )
		{
			if ( Autotiles is null ) return -1;

			var positions = new List<Vector2Int>();
			foreach ( var thing in Autotiles )
			{
				if ( !mergeAll && thing.Key != autotileId ) continue;
				foreach ( var pos in thing.Value )
				{
					if ( !positions.Contains( pos.Position ) )
						positions.Add( pos.Position );
				}
			}
			int value = 0;

			foreach ( var ride in overrides )
			{
				if ( ride.Value )
				{
					if ( !positions.Contains( ride.Key ) )
					{
						positions.Add( ride.Key );
					}
				}
				else
				{
					if ( positions.Contains( ride.Key ) )
					{
						positions.Remove( ride.Key );
					}
				}
			}

			var up = position.WithY( position.y + 1 );
			var down = position.WithY( position.y - 1 );
			var left = position.WithX( position.x - 1 );
			var right = position.WithX( position.x + 1 );
			var upLeft = up.WithX( left.x );
			var upRight = up.WithX( right.x );
			var downLeft = down.WithX( left.x );
			var downRight = down.WithX( right.x );

			foreach ( var pos in positions )
			{
				if ( pos == upLeft ) value += 1;
				if ( pos == up ) value += 2;
				if ( pos == upRight ) value += 4;
				if ( pos == left ) value += 8;
				if ( pos == right ) value += 16;
				if ( pos == downLeft ) value += 32;
				if ( pos == down ) value += 64;
				if ( pos == downRight ) value += 128;
			}

			return value;
		}

		public class AutotilePosition
		{
			public Vector2Int Position { get; set; }
			public bool ShouldMerge { get; set; } = false;

			public AutotilePosition ( Vector2Int position, bool shouldMerge = false )
			{
				Position = position;
				ShouldMerge = shouldMerge;
			}
		}
	}

	public class Tile
	{
		public Guid TileId { get; set; } = Guid.NewGuid();
		public Vector2Int CellPosition { get; set; }
		public bool HorizontalFlip { get; set; }
		public bool VerticalFlip { get; set; }
		public int Rotation { get; set; }
		public Vector2Int BakedPosition { get; set; }

		public Tile () { }

		public Tile ( Guid tileId, Vector2Int cellPosition, int rotation, bool flipX, bool flipY )
		{
			TileId = tileId;
			CellPosition = cellPosition;
			HorizontalFlip = flipX;
			VerticalFlip = flipY;
			Rotation = rotation;
		}

		public Tile Copy ()
		{
			return new Tile( TileId, CellPosition, Rotation, HorizontalFlip, VerticalFlip );
		}
	}

	public class ComponentControls { }

}

internal sealed class TilesetSceneObject : SceneCustomObject
{
	TilesetComponent Component;
	Dictionary<TilesetResource, (TileAtlas, Material)> Materials = new();
	Material MissingMaterial;
	int LayerIndex;

	public TilesetSceneObject ( TilesetComponent component, SceneWorld world, int layerIndex ) : base( world )
	{
		Component = component;
		LayerIndex = layerIndex;

		MissingMaterial = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
		MissingMaterial.Set( "Texture", Texture.Load( "images/missing-tile.png" ) );
		Tags.SetFrom( Component.Tags );
	}

	public override void RenderSceneObject ()
	{
		if ( Component?.Layers is null ) return;
		var Layer = Component.Layers.ElementAtOrDefault( LayerIndex );
		if ( Layer is null )
		{
			return;
		}

		var layers = Component.Layers.ToList();
		layers.Reverse();
		if ( layers.Count == 0 ) return;

		Dictionary<Vector2Int, TilesetComponent.Tile> missingTiles = new();

		if ( Layer?.IsVisible != true ) return;

		int i = 0;
		int layerIndex = layers.IndexOf( Layer );

		{
			var tileset = Layer.TilesetResource;
			if ( tileset is null ) return;
			var tilemap = tileset.TileMap;

			var combo = GetMaterial( tileset );
			if ( combo.Item1 is null || combo.Item2 is null ) return;

			var tiling = combo.Item1.GetTiling();
			var totalTiles = Layer.Tiles.Where( x => x.Value.TileId == default || tilemap.ContainsKey( x.Value.TileId ) );
			var vertex = ArrayPool<Vertex>.Shared.Rent( totalTiles.Count() * 6 );

			var minPosition = new Vector3( int.MaxValue, int.MaxValue, int.MaxValue );
			var maxPosition = new Vector3( int.MinValue, int.MinValue, int.MinValue );

			foreach ( var tile in Layer.Tiles )
			{
				var pos = tile.Key;
				Vector2Int offsetPos = Vector2Int.Zero;
				if ( tile.Value.TileId == default ) offsetPos = tile.Value.BakedPosition;
				else
				{
					if ( !tilemap.ContainsKey( tile.Value.TileId ) )
					{
						missingTiles[pos] = tile.Value;
						continue;
					}
					offsetPos = tilemap[tile.Value.TileId].Position;
				}
				var offset = combo.Item1.GetOffset( offsetPos + tile.Value.CellPosition );
				if ( tile.Value.HorizontalFlip )
					offset.x = -offset.x - tiling.x;
				if ( !tile.Value.VerticalFlip )
					offset.y = -offset.y - tiling.y;


				var size = tileset.GetTileSize();
				var position = new Vector3( pos.x, pos.y, Layer.Height ?? ( Component.Layers.Count - Component.Layers.IndexOf( Layer ) ) ) * new Vector3( size.x, size.y, 1 );

				minPosition = Vector3.Min( minPosition, position );
				maxPosition = Vector3.Max( maxPosition, position );

				var topLeft = new Vector3( position.x, position.y, position.z );
				var topRight = new Vector3( position.x + size.x, position.y, position.z );
				var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
				var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );

				var uvTopLeft = new Vector2( offset.x, offset.y );
				var uvTopRight = new Vector2( offset.x + tiling.x, offset.y );
				var uvBottomRight = new Vector2( offset.x + tiling.x, offset.y + tiling.y );
				var uvBottomLeft = new Vector2( offset.x, offset.y + tiling.y );

				if ( tile.Value.Rotation == 90 )
				{
					var tempUv = uvTopLeft;
					uvTopLeft = uvBottomLeft;
					uvBottomLeft = uvBottomRight;
					uvBottomRight = uvTopRight;
					uvTopRight = tempUv;
				}
				else if ( tile.Value.Rotation == 180 )
				{
					var tempUv = uvTopLeft;
					uvTopLeft = uvBottomRight;
					uvBottomRight = tempUv;
					tempUv = uvTopRight;
					uvTopRight = uvBottomLeft;
					uvBottomLeft = tempUv;
				}
				else if ( tile.Value.Rotation == 270 )
				{
					var tempUv = uvTopLeft;
					uvTopLeft = uvTopRight;
					uvTopRight = uvBottomRight;
					uvBottomRight = uvBottomLeft;
					uvBottomLeft = tempUv;
				}

				vertex[i] = new Vertex( topLeft );
				vertex[i].TexCoord0 = uvTopLeft;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( topRight );
				vertex[i].TexCoord0 = uvTopRight;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( bottomRight );
				vertex[i].TexCoord0 = uvBottomRight;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( topLeft );
				vertex[i].TexCoord0 = uvTopLeft;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( bottomRight );
				vertex[i].TexCoord0 = uvBottomRight;
				vertex[i].Normal = Vector3.Up;
				i++;

				vertex[i] = new Vertex( bottomLeft );
				vertex[i].TexCoord0 = uvBottomLeft;
				vertex[i].Normal = Vector3.Up;
				i++;
			}

			Graphics.Draw( vertex, totalTiles.Count() * 6, combo.Item2, Attributes );
			ArrayPool<Vertex>.Shared.Return( vertex );

			var siz = tileset.GetTileSize();
			maxPosition += new Vector3( siz.x, siz.y, 0 );
			Bounds = new BBox( minPosition, maxPosition + Vector3.Down * 0.01f ).Rotate( Rotation ).Translate( Position );


		}

		if ( missingTiles.Count > 0 )
		{
			var uvTopLeft = new Vector2( 0, 0 );
			var uvTopRight = new Vector2( 1, 0 );
			var uvBottomRight = new Vector2( 1, 1 );
			var uvBottomLeft = new Vector2( 0, 1 );

			foreach ( var tile in missingTiles )
			{
				var material = MissingMaterial;
				var pos = tile.Key;
				var size = Component.Layers[0].TilesetResource.TileSize;
				var position = new Vector3( pos.x, pos.y, 0 ) * new Vector3( size.x, size.y, 1 );

				var topLeft = new Vector3( position.x, position.y, position.z );
				var topRight = new Vector3( position.x + size.x, position.y, position.z );
				var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
				var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );

				var vertex = new Vertex[]
				{
				new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
				new Vertex(topRight) { TexCoord0 = uvTopRight, Normal = Vector3.Up },
				new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
				new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
				new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
				new Vertex(bottomLeft) { TexCoord0 = uvBottomLeft, Normal = Vector3.Up },
				};

				Graphics.Draw( vertex, 6, material, Attributes );
			}
		}
	}

	(TileAtlas, Material) GetMaterial ( TilesetResource resource )
	{
		var texture = TileAtlas.FromTileset( resource );

		if ( Materials.TryGetValue( resource, out var combo ) )
		{
			combo.Item1 = texture;
			combo.Item2.Set( "Texture", texture );
		}
		else
		{
			var material = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
			material.Set( "Texture", texture );
			combo.Item1 = texture;
			combo.Item2 = material;
			Materials.Add( resource, combo );
		}

		return combo;
	}
}
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SpriteTools;

/// <summary>
/// A class that re-packs a tileset with 1px borders to avoid bleeding.
/// </summary>
public class TileAtlas
{
	Texture Texture;
	Vector2 OriginalTileSize;
	Vector2Int TileSize;
	Vector2Int TileCounts;
	Dictionary<Vector2Int, Texture> TileCache = new();


	public static Dictionary<TilesetResource, TileAtlas> Cache = new();

	public Vector2 GetTiling ()
	{
		return (Vector2)OriginalTileSize / Texture.Size;
	}

	public Vector2 GetOffset ( Vector2Int cellPosition )
	{
		return new Vector2( cellPosition.x * TileSize.x + 1, cellPosition.y * TileSize.y + 1 ) / Texture.Size;
	}

	public static TileAtlas FromTileset ( TilesetResource tilesetResource )
	{
		if ( tilesetResource is null ) return null;

		if ( Cache?.ContainsKey( tilesetResource ) ?? false )
		{
			return Cache[tilesetResource];
		}

		if ( tilesetResource.Tiles.Count() == 0 )
		{
			return null;
		}

		if ( tilesetResource.Tiles.Any( x => x?.Tileset is null ) )
		{
			return null;
		}

		var path = tilesetResource.FilePath;
		if ( !FileSystem.Mounted.FileExists( path ) )
		{
			Log.Error( $"Tileset texture file {path} does not exist." );
			return null;
		}
		var texture = Texture.LoadFromFileSystem( path, FileSystem.Mounted );
		var atlas = new TileAtlas();

		var tileSize = tilesetResource.TileSize;
		atlas.TileSize = tileSize + Vector2Int.One * 2;
		atlas.OriginalTileSize = tileSize;

		var hTiles = tilesetResource.Tiles.Max( x => x.Position.x + x.Size.x );
		var vTiles = tilesetResource.Tiles.Max( x => x.Position.y + x.Size.y );
		atlas.TileCounts = new Vector2Int( hTiles, vTiles );

		var textureSize = new Vector2Int( hTiles * ( tileSize.x + 2 ), vTiles * ( tileSize.y + 2 ) );

		byte[] textureData = new byte[textureSize.x * textureSize.y * 4];
		for ( int i = 0; i < textureSize.x; i++ )
		{
			for ( int j = 0; j < textureSize.y; j++ )
			{
				var ind = ( j * textureSize.x + i ) * 4;
				textureData[ind] = 0;
				textureData[ind + 1] = 0;
				textureData[ind + 2] = 0;
				textureData[ind + 3] = 0;
			}
		}

		var pixels = texture.GetPixels();

		foreach ( var tile in tilesetResource.Tiles )
		{
			for ( int n = 0; n < tile.Size.x; n++ )
			{
				for ( int m = 0; m < tile.Size.y; m++ )
				{
					var cellPos = tile.Position + new Vector2Int( n, m );

					var tSize = tileSize * tile.Size;
					var tPos = cellPos * atlas.TileSize + Vector2Int.One;
					var sampleX = cellPos.x * tileSize.x;
					var sampleY = cellPos.y * tileSize.y;
					for ( int i = -1; i <= tSize.x; i++ )
					{
						for ( int j = -1; j <= tSize.y; j++ )
						{
							var sampleInd = (int)( ( sampleY + Math.Clamp( j, 0, tSize.y - 1 ) ) * texture.Size.x + sampleX + Math.Clamp( i, 0, tSize.x - 1 ) );
							var color = pixels[sampleInd];
							var ind = ( ( tPos.y + j ) * textureSize.x + tPos.x + i ) * 4;
							if ( ind < 0 || ind >= textureData.Length ) continue;
							textureData[ind + 0] = color.r;
							textureData[ind + 1] = color.g;
							textureData[ind + 2] = color.b;
							textureData[ind + 3] = color.a;
						}
					}


				}
			}

		}

		var builder = Texture.Create( textureSize.x, textureSize.y );
		builder.WithData( textureData );
		builder.WithMips( 0 );
		atlas.Texture = builder.Finish();

		Cache[tilesetResource] = atlas;

		return atlas;
	}

	public Texture GetTextureFromCell ( Vector2Int cellPosition )
	{
		if ( TileCache.ContainsKey( cellPosition ) )
		{
			return TileCache[cellPosition];
		}

		int x = cellPosition.x * TileSize.x + 1;
		int y = cellPosition.y * TileSize.y + 1;
		int outputSizeX = TileSize.x - 2;
		int outputSizeY = TileSize.y - 2;
		byte[] textureData = new byte[outputSizeX * outputSizeY * 4];
		var pixels = Texture.GetPixels();
		for ( int i = 0; i < outputSizeX; i++ )
		{
			for ( int j = 0; j < outputSizeY; j++ )
			{
				int ind = ( i + j * outputSizeX ) * 4;
				int sampleIndex = (int)( x + i + ( y + j ) * Texture.Size.x );
				var color = pixels[sampleIndex];
				textureData[ind + 0] = color.r;
				textureData[ind + 1] = color.g;
				textureData[ind + 2] = color.b;
				textureData[ind + 3] = color.a;
			}
		}

		var builder = Texture.Create( outputSizeX, outputSizeY );
		builder.WithData( textureData );
		builder.WithMips( 0 );
		var texture = builder.Finish();
		TileCache[cellPosition] = texture;
		return texture;
	}

	// Cast to texture
	public static implicit operator Texture ( TileAtlas atlas )
	{
		return atlas?.Texture ?? null;
	}

	public static void ClearCache ( string path = "" )
	{
		if ( path.StartsWith( "/" ) ) path = path.Substring( 1 );
		if ( string.IsNullOrEmpty( path ) )
		{
			Cache.Clear();
		}
		else
		{
			Cache = Cache.Where( x => x.Key.FilePath != path ).ToDictionary( x => x.Key, x => x.Value );
		}
	}

	public static void ClearCache ( TilesetResource tileset )
	{
		Cache.Remove( tileset );
	}
}
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace SpriteTools.TilesetEditor;

[EditorForAssetType( "tileset" )]
[EditorApp( "Tileset Editor", "calendar_view_month", "Edit Tilesets" )]
public partial class MainWindow : DockWindow, IAssetEditor
{
	internal static List<MainWindow> OpenWindows = new();
	public bool CanOpenMultipleAssets => false;

	private readonly UndoStack _undoStack = new();
	public UndoStack UndoStack => _undoStack;
	bool _dirty = true;

	private Asset _asset;
	public TilesetResource Tileset;
	[Property]
	public List<TilesetResource.Tile> SelectedTiles
	{
		get => inspector?.tileList?.Selected?.Select( x => x?.Tile )?.ToList() ?? new();
		set
		{
			inspector.tileList.Selected.Clear();
			foreach ( var tile in value )
			{
				var control = inspector.tileList.Buttons.FirstOrDefault( x => x.Tile == tile );
				if ( control != null )
				{
					inspector.tileList.Selected.Add( control );
				}
			}
		}
	}

	ToolBar toolBar;
	internal Inspector.Inspector inspector;
	internal Preview.Preview preview;

	Option _undoMenuOption;
	Option _redoMenuOption;

	public MainWindow ()
	{
		DeleteOnClose = true;

		Size = new Vector2( 1280, 720 );
		Tileset = new TilesetResource();

		SetWindowIcon( "emoji_emotions" );

		RestoreDefaultDockLayout();
		OpenWindows.Add( this );
	}

	public override void OnDestroyed ()
	{
		base.OnDestroyed();
		OpenWindows.Remove( this );
	}

	public void AssetOpen ( Asset asset )
	{
		Open( "", asset );
		Show();
	}

	public void SelectMember ( string memberName )
	{

	}

	void UpdateWindowTitle ()
	{
		Title = ( $"{_asset?.Name ?? "Untitled Tileset"} - Tileset Editor" ) + ( _dirty ? "*" : "" );
	}

	public void RebuildUI ()
	{
		MenuBar.Clear();

		{
			var file = MenuBar.AddMenu( "File" );
			file.AddOption( "New", "common/new.png", () => New(), "editor.new" ).StatusTip = "New Tileset";
			file.AddOption( "Open", "common/open.png", () => Open(), "editor.open" ).StatusTip = "Open Tileset";
			file.AddOption( "Save", "common/save.png", () => Save(), "editor.save" ).StatusTip = "Save Tileset";
			file.AddOption( "Save As...", "common/save.png", () => Save( true ), "editor.save-as" ).StatusTip = "Save Tileset As...";
			file.AddSeparator();
			file.AddOption( new Option( "Exit" ) { Triggered = Close } );
		}

		{
			var edit = MenuBar.AddMenu( "Edit" );
			_undoMenuOption = edit.AddOption( "Undo", "undo", () => Undo(), "editor.undo" );
			_redoMenuOption = edit.AddOption( "Redo", "redo", () => Redo(), "editor.redo" );

			// edit.AddSeparator();
			// edit.AddOption( "Cut", "common/cut.png", CutSelection, "Ctrl+X" );
			// edit.AddOption( "Copy", "common/copy.png", CopySelection, "Ctrl+C" );
			// edit.AddOption( "Paste", "common/paste.png", PasteSelection, "Ctrl+V" );
			// edit.AddOption( "Select All", "select_all", SelectAll, "Ctrl+A" );
		}

		{
			var view = MenuBar.AddMenu( "View" );

			view.AboutToShow += () => OnViewMenu( view );
		}

		CreateToolBar();

	}

	private void OnViewMenu ( Menu view )
	{
		view.Clear();
		view.AddOption( "Restore To Default", "settings_backup_restore", RestoreDefaultDockLayout );
		view.AddSeparator();

		foreach ( var dock in DockManager.DockTypes )
		{
			var o = view.AddOption( dock.Title, dock.Icon );
			o.Checkable = true;
			o.Checked = DockManager.IsDockOpen( dock.Title );
			o.Toggled += ( b ) => DockManager.SetDockState( dock.Title, b );
		}
	}

	protected override void RestoreDefaultDockLayout ()
	{
		inspector = new Inspector.Inspector( this );
		preview = new Preview.Preview( this );
		// Timeline = new Timeline.Timeline(this);
		// var animationList = new AnimationList.AnimationList(this);

		DockManager.Clear();
		DockManager.RegisterDockType( "Inspector", "edit", () => inspector = new Inspector.Inspector( this ) );
		DockManager.RegisterDockType( "Preview", "emoji_emotions", () => preview = new Preview.Preview( this ) );
		// DockManager.RegisterDockType("Animations", "directions_walk", () => new AnimationList.AnimationList(this));
		// DockManager.RegisterDockType("Timeline", "view_column", () =>
		// {
		//     Timeline = new Timeline.Timeline(this);
		//     return Timeline;
		// });

		DockManager.AddDock( null, inspector, DockArea.Left, DockManager.DockProperty.HideOnClose );
		DockManager.AddDock( null, preview, DockArea.Right, DockManager.DockProperty.HideOnClose, split: 0.8f );

		// DockManager.AddDock(preview, Timeline, DockArea.Bottom, DockManager.DockProperty.HideOnClose, split: 0.2f);
		// DockManager.AddDock(inspector, animationList, DockArea.Bottom, DockManager.DockProperty.HideOnClose, split: 0.45f);

		DockManager.Update();

		RebuildUI();
	}

	void InitInspector ()
	{
		inspector.segmentedControl.SelectedIndex = ( ( Tileset?.Tiles?.Count ?? 0 ) == 0 ) ? 0 : 1;
	}

	void UpdateEverything ()
	{
		UpdateWindowTitle();
		inspector.UpdateControlSheet();
		inspector.UpdateSelectedSheet();
		preview.UpdateTexture( Tileset.FilePath );
	}

	[Shortcut( "editor.new", "CTRL+N", ShortcutType.Window )]
	public void New ()
	{
		PromptSave( () => CreateNew() );
	}

	public void CreateNew ()
	{
		var savePath = GetSavePath( "New 2D Tileset" );

		_asset = null;
		Tileset = AssetSystem.CreateResource( "tileset", savePath ).LoadResource<TilesetResource>();
		_dirty = false;
		_undoStack.Clear();

		InitInspector();
		UpdateEverything();
	}

	[Shortcut( "editor.open", "CTRL+O", ShortcutType.Window )]
	public void Open ()
	{
		var fd = new FileDialog( null )
		{
			Title = "Open 2D Tileset",
			DefaultSuffix = ".tileset"
		};

		fd.SetNameFilter( "2D Tileset (*.tileset)" );

		if ( !fd.Execute() ) return;

		PromptSave( () => Open( fd.SelectedFile ) );
	}

	public void Open ( string path, Asset asset = null )
	{
		if ( !string.IsNullOrEmpty( path ) )
		{
			asset ??= AssetSystem.FindByPath( path );
		}
		if ( asset == null ) return;

		if ( asset == _asset )
		{
			Focus();
			return;
		}

		var tileset = asset.LoadResource<TilesetResource>();
		if ( tileset == null )
		{
			Log.Warning( $"Failed to load tileset from {asset.RelativePath}" );
			return;
		}

		StateCookie = "tileset-editor-window-" + tileset.ResourceId;

		_asset = asset;
		_dirty = false;
		_undoStack.Clear();

		Tileset = tileset;
		var firstTile = Tileset.Tiles?.FirstOrDefault();
		if ( firstTile is not null )
			inspector?.tileList?.Selected?.Add( inspector.tileList.Buttons.FirstOrDefault( x => x.Tile == firstTile ) );

		InitInspector();
		UpdateEverything();
	}

	private void Restore ()
	{
		var path = _asset?.AbsolutePath;
		if ( string.IsNullOrEmpty( path ) )
		{
			_dirty = false;
			return;
		}

		var contents = File.ReadAllText( path );
		ReloadFromString( contents );

		_dirty = false;
	}

	[Shortcut( "editor.save", "CTRL+S", ShortcutType.Window )]
	public bool Save ( bool saveAs = false )
	{
		var savePath = ( _asset == null || saveAs ) ? GetSavePath() : _asset.AbsolutePath;
		if ( string.IsNullOrWhiteSpace( savePath ) ) return false;

		if ( saveAs )
		{
			// If we're saving as, we want to register the new asset
			_asset = null;
		}

		// Register the asset if we haven't already
		_asset ??= AssetSystem.CreateResource( "tileset", savePath );
		_asset?.SaveToDisk( Tileset );
		_dirty = false;
		UpdateWindowTitle();

		if ( _asset == null )
		{
			Log.Warning( $"Failed to register asset at path {savePath}" );
			return false;
		}

		MainAssetBrowser.Instance?.Local?.UpdateAssetList();
		TileAtlas.ClearCache( Tileset );

		return true;
	}

	[Shortcut( "editor.save-as", "CTRL+SHIFT+S", ShortcutType.Window )]
	private void SaveAs ()
	{
		Save( true );
	}

	[EditorEvent.Frame]
	void Frame ()
	{
		_undoOption.Enabled = _undoStack.CanUndo;
		_redoOption.Enabled = _undoStack.CanRedo;
		_undoMenuOption.Enabled = _undoStack.CanUndo;
		_redoMenuOption.Enabled = _undoStack.CanRedo;

		_undoOption.Text = _undoStack.UndoName ?? "Undo";
		_redoOption.Text = _undoStack.RedoName ?? "Redo";
		_undoMenuOption.Text = _undoStack.UndoName ?? "Undo";
		_redoMenuOption.Text = _undoStack.RedoName ?? "Redo";

		_undoOption.StatusTip = _undoStack.UndoName ?? "Undo";
		_redoOption.StatusTip = _undoStack.RedoName ?? "Redo";
		_undoMenuOption.StatusTip = _undoStack.UndoName ?? "Undo";
		_redoMenuOption.StatusTip = _undoStack.RedoName ?? "Redo";
	}

	protected override bool OnClose ()
	{
		if ( _dirty )
		{
			var confirm = new PopupWindow(
				"Save Current Tileset", "The open tileset has unsaved changes. Would you like to save now?", "Cancel",
				new Dictionary<string, System.Action>()
				{
					{ "No", () => { Restore(); Close(); } },
					{ "Yes", () => { Save(); Close(); } }
				}
			);

			confirm.Show();

			return false;
		}

		return true;
	}

	string GetSavePath ( string title = "Save Tileset" )
	{
		var lastDirectory = EditorCookie.GetString( "LastSaveTilesetLocation", "" );
		var fd = new FileDialog( null )
		{
			Title = title,
			Directory = lastDirectory,
			DefaultSuffix = $".tileset"
		};

		fd.SelectFile( "untitled.tileset" );
		fd.SetFindFile();
		fd.SetModeSave();
		fd.SetNameFilter( "2D Tileset (*.tileset)" );
		if ( !fd.Execute() ) return null;

		var selectedFile = fd.SelectedFile;
		EditorCookie.SetString( "LastSaveTilesetLocation", System.IO.Path.GetDirectoryName( selectedFile ) );
		return selectedFile;
	}

	internal void CreateTile ( int x, int y, bool add = false )
	{
		var tileName = $"Tile {x},{y}";

		PushUndo( $"Create Tile \"{tileName}\"" );
		var tile = new TilesetResource.Tile( new Vector2Int( x, y ), 1 );
		Tileset.AddTile( tile );

		if ( Tileset.Tiles.Count == 1 )
		{
			Tileset.CurrentTileSize = Tileset.TileSize;
			Tileset.CurrentTextureSize = (Vector2Int)preview.TextureSize;
			inspector.UpdateControlSheet();
		}
		else
		{
			var control = new TilesetTileControl( inspector.tileList, tile );
			inspector.tileList.content.Add( control );
			inspector.tileList.Buttons.Add( control );
		}

		SelectTile( tile, add );
		PushRedo();
		SetDirty();
	}

	internal void SelectTile ( TilesetResource.Tile tile, bool add = false )
	{
		var btn = inspector.tileList.Buttons.FirstOrDefault( x => x.Tile == tile );
		if ( add )
		{
			if ( inspector.tileList.Selected.Contains( btn ) )
				inspector.tileList.Selected.Remove( btn );
			else
				inspector.tileList.Selected.Add( btn );
		}
		else
		{
			inspector.tileList.Selected.Clear();
			inspector.tileList.Selected.Add( btn );
		}
		inspector.UpdateSelectedSheet();
	}

	internal void DeleteTile ( TilesetResource.Tile tile )
	{
		var tileName = tile.Name;
		if ( string.IsNullOrEmpty( tileName ) ) tileName = $"Tile {tile.Position}";

		PushUndo( $"Delete Tile \"{tileName}\"" );
		bool isSelected = inspector.tileList.Selected.Any( x => x.Tile == tile );
		Tileset.RemoveTile( tile );

		if ( isSelected ) SelectTile( Tileset.Tiles?.FirstOrDefault() ?? null );
		PushRedo();

		if ( Tileset.Tiles.Count == 0 )
		{
			inspector.UpdateControlSheet();
		}
		else
		{
			var btns = inspector.tileList.Buttons.ToList();
			foreach ( var btn in btns )
			{
				if ( btn.Tile == tile )
				{
					inspector.tileList.Buttons.Remove( btn );
					btn.Destroy();
				}
			}
		}
		SetDirty();
	}

	internal void GenerateTiles ()
	{
		if ( Tileset is null ) return;

		PushUndo( "Generate Tiles" );
		foreach ( var tile in Tileset.Tiles.ToList() )
		{
			Tileset.RemoveTile( tile );
		}
		Tileset.CurrentTileSize = Tileset.TileSize;
		Tileset.CurrentTextureSize = (Vector2Int)preview.TextureSize;

		int x = 0;
		int y = 0;
		int framesPerRow = (int)preview.TextureSize.x / Tileset.TileSize.x;
		int framesPerHeight = (int)preview.TextureSize.y / Tileset.TileSize.y;
		var pixels = Texture.LoadFromFileSystem( Tileset.FilePath, Editor.FileSystem.Mounted ).GetPixels();

		while ( y < framesPerHeight )
		{
			while ( x < framesPerRow )
			{
				var hasPixel = false;
				for ( var xx = 0; xx < Tileset.TileSize.x; xx++ )
				{
					for ( var yy = 0; yy < Tileset.TileSize.y; yy++ )
					{
						var tx = x * Tileset.TileSize.x + xx;
						var ty = y * Tileset.TileSize.y + yy;
						int pixelIndex = (int)( ty * preview.TextureSize.x + tx );
						if ( pixels[pixelIndex].a > 0 )
						{
							hasPixel = true;
							break;
						}
					}
					if ( hasPixel ) break;
				}
				if ( hasPixel )
				{
					Tileset.AddTile( new TilesetResource.Tile( new Vector2Int( x, y ), 1 ) );
				}
				x++;
			}
			x = 0;
			y++;
		}
		PushRedo();
		SetDirty();
		UpdateEverything();
	}

	internal void DeleteAllTiles ()
	{
		if ( Tileset is null ) return;

		PushUndo( "Delete All Tiles" );
		Tileset.Tiles ??= new List<TilesetResource.Tile>();
		Tileset.Tiles?.Clear();
		PushRedo();
		SetDirty();
		UpdateEverything();
	}

	void PromptSave ( Action action )
	{
		if ( !_dirty )
		{
			action?.Invoke();
			return;
		}

		var confirm = new PopupWindow(
			"Save Current Tileset", "The open tileset has unsaved changes. Would you like to save before continuing?", "Cancel",
			new Dictionary<string, Action>
			{
				{ "No", () => {
					action?.Invoke();
				} },
				{ "Yes", () => {
					if (Save()) action?.Invoke();
				}}
			} );
		confirm.Show();
	}

	internal void SetDirty ()
	{
		_dirty = true;
		UpdateWindowTitle();
	}

	public void PushUndo ( string name, string buffer = "" )
	{
		if ( string.IsNullOrEmpty( buffer ) ) buffer = Tileset.SerializeString();
		_undoStack.PushUndo( name, buffer );
	}

	public void PushRedo ()
	{
		_undoStack.PushRedo( Tileset.SerializeString() );
		SetDirty();
	}

	[Shortcut( "editor.undo", "CTRL+Z", ShortcutType.Window )]
	public void Undo ()
	{
		if ( _undoStack.Undo() is UndoOp op )
		{
			ReloadFromString( op.undoBuffer );
			Sound.Play( "ui.navigate.back" );
		}
		else
		{
			Sound.Play( "ui.navigate.deny" );
		}
	}

	private void SetUndoLevel ( int level )
	{
		if ( _undoStack.SetUndoLevel( level ) is UndoOp op )
		{
			ReloadFromString( op.undoBuffer );
		}
	}

	[Shortcut( "editor.redo", "CTRL+Y", ShortcutType.Window )]
	public void Redo ()
	{
		if ( _undoStack.Redo() is UndoOp op )
		{
			ReloadFromString( op.redoBuffer );
			Sound.Play( "ui.navigate.forward" );
		}
		else
		{
			Sound.Play( "ui.navigate.deny" );
		}
	}

	internal void ReloadFromString ( string buffer )
	{
		Tileset.DeserializeString( buffer );

		SetDirty();
		UpdateEverything();
	}

	private Option _undoOption;
	private Option _redoOption;

	private void CreateToolBar ()
	{
		toolBar?.Destroy();
		toolBar = new ToolBar( this, "TilesetEditorToolbar" );
		AddToolBar( toolBar, ToolbarPosition.Top );

		toolBar.AddOption( "New", "common/new.png", New ).StatusTip = "New Tileset";
		toolBar.AddOption( "Open", "common/open.png", Open ).StatusTip = "Open Tileset";
		toolBar.AddOption( "Save", "common/save.png", () => Save() ).StatusTip = "Save Tileset";

		toolBar.AddSeparator();

		_undoOption = toolBar.AddOption( "Undo", "undo", Undo );
		_redoOption = toolBar.AddOption( "Redo", "redo", Redo );

		toolBar.AddSeparator();

		toolBar.AddSeparator();

		_undoOption.Enabled = false;
		_redoOption.Enabled = false;
	}
}
using Editor;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace SpriteTools.TilesetTool.Tools;

/// <summary>
/// Used to paint tiles on the selected layer.
/// </summary>
[Title( "Paint" )]
[Icon( "brush" )]
[Alias( "tileset-tools.paint-tool" )]
[Group( "1" )]
[Order( 0 )]
public class PaintTileTool : BaseTileTool
{
	public PaintTileTool ( TilesetTool parent ) : base( parent ) { }

	/// <summary>
	/// The size of the Brush when Painting.
	/// </summary>
	[Group( "Paint Tool" ), Property, Range( 1, 12 ), Step( 1 )]
	public int BrushSize
	{
		get => _brushSize;
		set
		{
			_brushSize = value;
			lastTilePos = -999999;
		}
	}
	private int _brushSize = 1;

	/// <summary>
	/// Whether the Brush is round or square.
	/// </summary>
	[Group( "Paint Tool" ), Property]
	public bool IsRound
	{
		get => _isRound;
		set
		{
			_isRound = value;
			lastTilePos = -999999;
		}
	}
	private bool _isRound = false;

	/// <summary>
	/// If enabled, Autotiles of different types will attempt to connect with each other.
	/// </summary>
	[Group( "Paint Tool" ), Property, ShowIf( nameof( this.CanSeeAutotileSettings ), true )]
	public bool MergeDifferentAutotiles
	{
		get => ShouldMergeAutotiles;
		set
		{
			ShouldMergeAutotiles = value;
		}
	}
	private bool CanSeeAutotileSettings => AutotileWidget.Instance?.Brush is not null;

	Vector2Int lastTilePos;
	bool isPainting = false;

	public override void OnUpdate ()
	{
		if ( !CanUseTool() ) return;
		if ( Parent.SelectedComponent.Transform is null ) return;

		var pos = GetGizmoPos();
		var tile = Parent.SelectedTile;
		var tilePos = (Vector2Int)( ( pos - Parent.SelectedComponent.WorldPosition ) / Parent.SelectedLayer.TilesetResource.GetTileSize() );

		Parent._sceneObject.Transform = new Transform( pos, Rotation.Identity, 1 );
		Parent._sceneObject.RenderingEnabled = true;

		if ( tilePos != lastTilePos )
		{
			UpdateTilePositions();
		}

		if ( Gizmo.IsLeftMouseDown )
		{
			if ( _componentUndoScope is null )
			{
				_componentUndoScope = SceneEditorSession.Active.UndoScope( "Paint Tile" ).WithComponentChanges( Parent.SelectedComponent ).Push();
			}
			var brush = AutotileBrush;
			if ( brush is not null )
			{
				Place( tilePos );
			}
			else if ( tile.Size.x > 1 || tile.Size.y > 1 )
			{
				for ( int x = 0; x < tile.Size.x; x++ )
				{
					var ux = x;
					var xx = x;
					if ( Parent.Settings.HorizontalFlip ) ux = tile.Size.x - x - 1;
					for ( int y = 0; y < tile.Size.y; y++ )
					{
						var uy = y;
						var yy = -y;
						var offsetPos = new Vector2Int( xx, yy );

						if ( Parent.Settings.Angle == 90 )
							offsetPos = new Vector2Int( -offsetPos.y, offsetPos.x );
						else if ( Parent.Settings.Angle == 180 )
							offsetPos = new Vector2Int( -offsetPos.x, -offsetPos.y );
						else if ( Parent.Settings.Angle == 270 )
							offsetPos = new Vector2Int( offsetPos.y, -offsetPos.x );

						Parent.PlaceTile( tilePos + offsetPos, tile.Id, new Vector2Int( ux, uy ), false );
					}
				}
				Parent.SelectedComponent.IsDirty = true;
			}
			else
			{
				Place( tilePos );
			}
			isPainting = true;
		}
		else if ( isPainting )
		{
			_componentUndoScope?.Dispose();
			_componentUndoScope = null;
			isPainting = false;
		}

		// if (Parent?.SelectedLayer?.AutoTilePositions is not null)
		// {
		//     var tileSize = Parent.SelectedLayer.TilesetResource.GetTileSize();
		//     using (Gizmo.Scope("test", Transform.Zero))
		//     {
		//         Gizmo.Draw.Color = Color.Red.WithAlpha(0.1f);
		//         foreach (var group in Parent.SelectedLayer.AutoTilePositions)
		//         {
		//             var brush = group.Key;
		//             foreach (var position in group.Value)
		//             {
		//                 Gizmo.Draw.WorldText(Parent.SelectedLayer.GetAutotileBitmask(brush, position).ToString(),
		//                     new Transform(
		//                         Parent.SelectedComponent.WorldPosition + (Vector3)((Vector2)position * tileSize) + (Vector3)(tileSize * 0.5f) + Vector3.Up * 200,
		//                         Rotation.Identity,
		//                         0.3f
		//                     ),
		//                     "Poppins", 24
		//                 );
		//             }
		//         }
		//     }
		// }
	}

	void UpdateTilePositions ()
	{
		var pos = GetGizmoPos();
		var brush = AutotileBrush;
		var tile = Parent.SelectedTile;
		if ( tile is null ) return;
		var tilePos = (Vector2Int)( ( pos - Parent.SelectedComponent.WorldPosition ) / Parent.SelectedLayer.TilesetResource.GetTileSize() );

		List<(Vector2Int, Vector2Int)> positions = new();
		if ( brush is null && ( tile.Size.x > 1 || tile.Size.y > 1 ) )
		{
			for ( int i = 0; i < tile.Size.x; i++ )
			{
				for ( int j = 0; j < tile.Size.y; j++ )
				{
					positions.Add( (new Vector2Int( i, -j ), tile.Position + new Vector2Int( i, j )) );
				}
			}
		}
		else if ( IsRound )
		{
			var size = ( BrushSize - 0.9f ) * 2;
			var center = new Vector2Int( (int)( size / 2f ), (int)( size / 2f ) );
			for ( int i = 0; i < size * 2; i++ )
			{
				for ( int j = 0; j < size * 2; j++ )
				{
					var offset = new Vector2Int( i, j ) - center;
					if ( offset.LengthSquared <= ( size / 2 ) * ( size / 2 ) )
					{
						positions.Add( (offset, tile.Position) );
					}
				}
			}
		}
		else
		{
			Vector2Int startPos = new Vector2Int( -BrushSize / 2, -BrushSize / 2 );
			for ( int i = 0; i < BrushSize; i++ )
			{
				for ( int j = 0; j < BrushSize; j++ )
				{
					positions.Add( (new Vector2Int( i, j ) + startPos, tile.Position) );
				}
			}
		}

		// Set autobrush tiles if necessary
		if ( brush is not null )
		{
			if ( brush.AutotileType == AutotileType.Bitmask2x2Edge )
			{
				List<Vector2Int> tilesToAdd = new();
				foreach ( var ppos in positions )
				{
					bool touchingX = false;
					bool touchingY = false;
					var up = ppos.Item1.WithY( ppos.Item1.y + 1 );
					var down = ppos.Item1.WithY( ppos.Item1.y - 1 );
					var left = ppos.Item1.WithX( ppos.Item1.x - 1 );
					var right = ppos.Item1.WithX( ppos.Item1.x + 1 );
					foreach ( var ppos2 in positions )
					{
						if ( !touchingX && ( ppos2.Item1 == left || ppos2.Item1 == right ) )
						{
							touchingX = true;
						}
						if ( !touchingY && ( ppos2.Item1 == up || ppos2.Item1 == down ) )
						{
							touchingY = true;
						}
						if ( touchingX && touchingY ) break;
					}
					if ( touchingX && touchingY ) continue;

					var upLeft = up.WithX( left.x );
					var upRight = up.WithX( right.x );
					var downLeft = down.WithX( left.x );
					var downRight = down.WithX( right.x );
					if ( !tilesToAdd.Contains( up ) ) tilesToAdd.Add( up );
					if ( !tilesToAdd.Contains( down ) ) tilesToAdd.Add( down );
					if ( !tilesToAdd.Contains( left ) ) tilesToAdd.Add( left );
					if ( !tilesToAdd.Contains( right ) ) tilesToAdd.Add( right );
					if ( !tilesToAdd.Contains( upLeft ) ) tilesToAdd.Add( upLeft );
					if ( !tilesToAdd.Contains( upRight ) ) tilesToAdd.Add( upRight );
					if ( !tilesToAdd.Contains( downLeft ) ) tilesToAdd.Add( downLeft );
					if ( !tilesToAdd.Contains( downRight ) ) tilesToAdd.Add( downRight );
				}
				foreach ( var toAddPos in tilesToAdd )
				{
					if ( !positions.Contains( (toAddPos, tile.Position) ) )
						positions.Add( (toAddPos, tile.Position) );
				}
			}
		}

		UpdateTilePositions( positions.Select( x => (Vector2)x.Item1 ).ToList() );
		lastTilePos = tilePos;
	}

	void Place ( Vector2Int tilePos )
	{
		var brush = AutotileBrush;
		var tile = Parent.SelectedTile;


		foreach ( var position in Parent._sceneObject.MultiTilePositions )
		{
			if ( brush is null )
			{
				Parent.PlaceTile( tilePos + position.Item1, tile.Id, 0 );
			}
			else
			{
				Parent.PlaceAutotile( ( position.Item3 == Guid.Empty ) ? brush.Id : position.Item3, tilePos + position.Item1, false );
			}
		}

		if ( brush is not null )
		{
			foreach ( var position in Parent._sceneObject.MultiTilePositions )
			{
				var brushId = ( position.Item3 == Guid.Empty ) ? brush.Id : position.Item3;
				Parent.SelectedLayer.UpdateAutotile( brushId, tilePos + position.Item1, false, shouldMerge: MergeDifferentAutotiles );
			}
		}

		return;
	}

	[Shortcut( "tileset-tools.paint-tool", "b", typeof( SceneViewportWidget ) )]
	public static void ActivateSubTool ()
	{
		if ( EditorToolManager.CurrentModeName != nameof( TilesetTool ) ) return;
		EditorToolManager.SetSubTool( nameof( PaintTileTool ) );
	}
}
using Editor;
using Sandbox;
using System;
using System.Linq;

namespace SpriteTools.SpriteEditor.Preview;

public class Preview : Widget
{
	public MainWindow MainWindow { get; }
	private readonly RenderingWidget Rendering;

	Widget Overlay;
	WidgetWindow overlayWindowZoom;
	WidgetWindow overlayWindowPoint;

	Vector2 attachmentCreatePosition;

	public Preview ( MainWindow mainWindow ) : base( null )
	{
		MainWindow = mainWindow;

		Name = "Preview";
		WindowTitle = "Preview";
		SetWindowIcon( "emoji_emotions" );

		MinimumSize = new Vector2( 256, 256 );

		Layout = Layout.Column();

		Rendering = new RenderingWidget( MainWindow, this );
		Layout.Add( Rendering );

		Overlay = new Widget( this )
		{
			Layout = Layout.Row(),
			TranslucentBackground = true,
			NoSystemBackground = true,
			WindowFlags = WindowFlags.FramelessWindowHint | WindowFlags.Tool
		};
		overlayWindowZoom = new WidgetWindow( this );
		overlayWindowZoom.Parent = Overlay;
		overlayWindowZoom.Layout = Layout.Row();
		overlayWindowZoom.Layout.Spacing = 4;
		overlayWindowZoom.Layout.Margin = 4;
		var btnZoomOut = overlayWindowZoom.Layout.Add( new IconButton( "zoom_out" ) );
		btnZoomOut.OnClick = () =>
		{
			Rendering.Zoom( -250 );
		};
		btnZoomOut.ToolTip = "Zoom Out";
		btnZoomOut.StatusTip = "Zoom Out View";
		var btnZoomIn = overlayWindowZoom.Layout.Add( new IconButton( "zoom_in" ) );
		btnZoomIn.OnClick = () =>
		{
			Rendering.Zoom( 250 );
		};
		btnZoomIn.ToolTip = "Zoom In";
		btnZoomIn.StatusTip = "Zoom In View";
		overlayWindowZoom.Layout.AddSeparator();
		var btnFit = overlayWindowZoom.Layout.Add( new IconButton( "zoom_out_map" ) );
		btnFit.OnClick = () =>
		{
			Rendering.Fit();
		};
		btnFit.ToolTip = "Fit to Screen";
		btnFit.StatusTip = "Fit View to Screen";
		overlayWindowZoom.WindowTitle = "Zoom Controls";

		Overlay.Layout.Add( overlayWindowZoom );

		overlayWindowPoint = new WidgetWindow( this );
		overlayWindowPoint.Parent = Overlay;
		overlayWindowPoint.Layout = Layout.Column();
		overlayWindowPoint.Layout.Margin = 4;
		overlayWindowPoint.WindowTitle = "Point Controls";

		var row1 = overlayWindowPoint.Layout.AddRow();
		var btnTopLeft = row1.Add( new TextureModifyButton( this, "Align Top-Left", "Images/grid-align-top-left.png", () => SetOrigin( new Vector2( 0, 0f ) ) ) );
		var btnTopMiddle = row1.Add( new TextureModifyButton( this, "Align Top-Center", "Images/grid-align-top-center.png", () => SetOrigin( new Vector2( 0.5f, 0f ) ) ) );
		var btnTopRight = row1.Add( new TextureModifyButton( this, "Align Top-Right", "Images/grid-align-top-right.png", () => SetOrigin( new Vector2( 1f, 0f ) ) ) );

		var row2 = overlayWindowPoint.Layout.AddRow();
		var btnMiddleLeft = row2.Add( new TextureModifyButton( this, "Align Middle-Left", "Images/grid-align-middle-left.png", () => SetOrigin( new Vector2( 0f, 0.5f ) ) ) );
		var btnMiddleCenter = row2.Add( new TextureModifyButton( this, "Align Middle-Center", "Images/grid-align-middle-center.png", () => SetOrigin( new Vector2( 0.5f, 0.5f ) ) ) );
		var btnMiddleRight = row2.Add( new TextureModifyButton( this, "Align Middle-Right", "Images/grid-align-middle-right.png", () => SetOrigin( new Vector2( 1f, 0.5f ) ) ) );

		var row3 = overlayWindowPoint.Layout.AddRow();
		var btnBottomLeft = row3.Add( new TextureModifyButton( this, "Align Bottom-Left", "Images/grid-align-bottom-left.png", () => SetOrigin( new Vector2( 0, 1f ) ) ) );
		var btnBottomCenter = row3.Add( new TextureModifyButton( this, "Align Bottom-Center", "Images/grid-align-bottom-center.png", () => SetOrigin( new Vector2( 0.5f, 1f ) ) ) );
		var btnBottomRight = row3.Add( new TextureModifyButton( this, "Align Bottom-Right", "Images/grid-align-bottom-right.png", () => SetOrigin( new Vector2( 1f, 1f ) ) ) );
		Overlay.Layout.Add( overlayWindowPoint );

		Overlay.Show();
		UpdateTexture();
		SetSizeMode( SizeMode.Default, SizeMode.CanShrink );

		MainWindow.OnTextureUpdate += UpdateTexture;

		MainWindow.Moved += DoLayout;
	}

	public override void OnDestroyed ()
	{
		base.OnDestroyed();

		MainWindow.OnTextureUpdate -= UpdateTexture;
		MainWindow.Moved -= DoLayout;
	}

	void SetOrigin ( Vector2 origin )
	{
		if ( MainWindow.SelectedAnimation is null ) return;
		MainWindow.SelectedAnimation.Origin = origin;
	}

	void UpdateTexture ()
	{
		if ( MainWindow.Sprite is null ) return;
		if ( MainWindow.SelectedAnimation is null ) return;
		if ( MainWindow.SelectedAnimation.Frames.Count <= 0 ) return;
		if ( MainWindow.CurrentFrameIndex < 0 ) return;
		if ( MainWindow.CurrentFrameIndex >= MainWindow.SelectedAnimation.Frames.Count ) return;
		var frame = MainWindow.SelectedAnimation.Frames[MainWindow.CurrentFrameIndex];
		if ( frame is null ) return;
		if ( !Sandbox.FileSystem.Mounted.FileExists( frame.FilePath ) ) return;
		var texture = Texture.LoadFromFileSystem( frame.FilePath, Sandbox.FileSystem.Mounted );
		if ( texture is null ) return;
		Rendering.SetTexture( texture, frame.SpriteSheetRect );
	}

	protected override void DoLayout ()
	{
		base.DoLayout();

		if ( Overlay.IsValid() && Rendering.IsValid() )
		{
			Overlay.Position = Rendering.ScreenPosition;
			Overlay.Size = Rendering.Size + 1;

			if ( overlayWindowPoint.Visible )
			{
				overlayWindowPoint.AdjustSize();
				overlayWindowPoint.AlignToParent( TextFlag.LeftTop, 4 );
			}

			overlayWindowZoom.AdjustSize();
			overlayWindowZoom.AlignToParent( TextFlag.RightTop, 4 );
		}
	}

	protected override void OnVisibilityChanged ( bool visible )
	{
		base.OnVisibilityChanged( visible );

		if ( Overlay is not null )
		{
			Overlay.Visible = visible;
		}
	}

	protected override void OnKeyRelease ( KeyEvent e )
	{
		base.OnKeyRelease( e );

		if ( e.Key == KeyCode.Left )
		{
			MainWindow.FramePrevious();
		}
		else if ( e.Key == KeyCode.Right )
		{
			MainWindow.FrameNext();
		}
	}

	void CreateAttachmentPopup ()
	{
		var popup = new PopupWidget( MainWindow );
		popup.Layout = Layout.Column();
		popup.Layout.Margin = 16;
		popup.Layout.Spacing = 8;

		popup.Layout.Add( new Label( $"What would you like to name the attachment point?" ) );

		var entry = new LineEdit( popup );
		var button = new Button.Primary( "Create" );

		button.MouseClick = () =>
		{
			if ( !string.IsNullOrEmpty( entry.Text ) && !MainWindow.SelectedAnimation.Attachments.Any( a => a.Name.ToLowerInvariant() == entry.Text.ToLowerInvariant() ) )
			{
				CreateAttachment( entry.Text );
			}
			else
			{
				ShowNamingError( entry.Text );
			}
			popup.Visible = false;
		};

		entry.ReturnPressed += button.MouseClick;

		popup.Layout.Add( entry );

		var bottomBar = popup.Layout.AddRow();
		bottomBar.AddStretchCell();
		bottomBar.Add( button );

		popup.Position = Editor.Application.CursorPosition;
		popup.Visible = true;

		entry.Focus();
	}

	void CreateAttachment ( string name )
	{
		MainWindow.PushUndo( "Add Attachment Point " + name );
		var tr = Rendering.Scene.Trace.Ray( Rendering.Camera.ScreenPixelToRay( attachmentCreatePosition ), 5000f ).Run();
		var pos = tr.EndPosition.WithZ( 0f );
		var attachPos = new Vector2( pos.y, pos.x );
		attachPos = ( attachPos / 100f ) + ( Vector2.One * 0.5f );
		var attachment = new SpriteAttachment( name );
		attachment.Points.Add( attachPos );
		MainWindow.SelectedAnimation.Attachments.Add( attachment );
		// MainWindow.SelectedAnimation.Frames[MainWindow.CurrentFrameIndex].AttachmentPoints[name] = attachPos;
		MainWindow.PushRedo();
	}

	static void ShowNamingError ( string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
		{
			var confirm = new PopupWindow( "Invalid name ''", "You cannot give an attachment point an empty name", "OK" );
			confirm.Show();

			return;
		}

		var confirm2 = new PopupWindow( "Invalid name", $"An attachment point named '{name}' already exists", "OK" );
		confirm2.Show();
	}

	protected override void OnContextMenu ( ContextMenuEvent e )
	{
		base.OnContextMenu( e );

		attachmentCreatePosition = e.LocalPosition;

		var m = new Menu( this );

		m.AddOption( "Add Attach Point", "push_pin", CreateAttachmentPopup );

		m.OpenAtCursor( false );
	}


	private class TextureModifyButton : Widget
	{
		private readonly string Icon;
		private Pixmap pixmap;

		public TextureModifyButton ( Widget parent, string tooltip, string icon, Action onClick ) : base( parent )
		{
			Icon = icon;
			ToolTip = tooltip;
			FixedSize = 26;
			Cursor = CursorShape.Finger;
			MouseClick = onClick;
			var fullPath = Editor.FileSystem.Content.GetFullPath( Icon );
			if ( fullPath is not null )
			{
				pixmap = Pixmap.FromFile( fullPath );
			}
		}

		protected override void OnPaint ()
		{
			base.OnPaint();

			if ( Paint.HasMouseOver )
			{
				Paint.ClearPen();
				var bg = Theme.ControlBackground.Lighten( 0.3f );
				Paint.SetBrush( bg );
				Paint.DrawRect( LocalRect, Theme.ControlRadius );
			}
			if ( pixmap is not null )
			{
				Paint.Draw( LocalRect.Shrink( 5 ), pixmap );
			}
		}
	}
}
using System.Collections.Generic;

namespace Sandbox;

public static class TraceExtensions
{
	extension( SceneTrace trace )
	{
		/// <summary>
		/// Performs a ray trace along the given ray for the specified distance,
		/// returning only the first hit.
		/// </summary>
		/// <param name="ray">The ray to trace along.</param>
		/// <param name="distance">How far the ray should travel.</param>
		/// <param name="withTags">Optional tags to restrict the trace to objects that have any of these tags.</param>
		/// <returns>Result of the trace.</returns>
		public SceneTraceResult RunRayTrace( Ray ray, float distance = 100f, params string[] withTags )
		{
			return trace.Ray( ray, distance )
				.WithAnyTags( withTags )
				.Run();
		}
		
		/// <summary>
		/// Performs a ray trace along the given ray for the specified distance,
		/// returning all hits along the ray.
		/// </summary>
		/// <param name="ray">The ray to trace along.</param>
		/// <param name="distance">How far the ray should travel.</param>
		/// <param name="withTags">Optional tags to restrict the trace to objects that have any of these tags.</param>
		/// <returns>All results of the trace in order along the ray.</returns>
		public IEnumerable<SceneTraceResult> RunAllRayTrace( Ray ray, float distance = 100f, params string[] withTags )
		{
			return trace.Ray( ray, distance )
				.WithAnyTags( withTags )
				.RunAll();
		}
	}
}
using System.Collections.Generic;
using Sandbox;
using Sandbox.Diagnostics;

namespace WackyLib.Patterns;

/// <summary>
/// Represents a Singleton component. Which is a component that we can access through an instance, and only one can be allowed in a scene at once.
/// In-game, duplicate instances are automatically destroyed. In the editor (with ExecuteInEditor), multiple instances are permitted to coexist
/// for tooling purposes, but a warning is logged, as only one instance will survive when the game runs (the latest awakened component).
/// </summary>
public abstract class Singleton<T> : Component, IHotloadManaged where T : Singleton<T>
{
#nullable enable
#pragma warning disable SB3000
	// ReSharper disable once MemberCanBePrivate.Global
	public static T? Instance { get; private set; }
#pragma warning restore SB3000
	
	private readonly Logger Log = new( "Singleton" );
	
	protected override void OnAwake()
	{
		// We're running ExecuteInEditor, which means we should ignore instances.
		if ( ExecutingInEditor() )
		{
			Log.Info( $"OnAwake called in editor with ExecuteInEditor, creating once." );
			
			if ( Active )
			{
				if ( Instance.IsValid() && Instance != this )
				{
					Log.Warning( $"Multiple {typeof(T)} instances detected in the scene! Only one will be used in-game." );
				}
				
				Instance = (T)this;
			}
			
			return;
		}
		
		if ( Instance.IsValid() )
		{
			Log.Warning( $"Singleton tried to initialize another {typeof(T)}!" );
			Destroy();
			return;
		}
		
		if ( Active )
		{
			Instance = (T)this;
		}
	}
	
	protected override void OnDestroy()
	{
		if ( Instance == this )
		{
			Instance = null;
		}
	}
	
	void IHotloadManaged.Destroyed( Dictionary<string, object> state )
	{
		state["IsActive"] = Instance == this;
	}
	
	void IHotloadManaged.Created( IReadOnlyDictionary<string, object> state )
	{
		if ( state.GetValueOrDefault( "IsActive" ) is true )
		{
			Instance = (T)this;
		}
	}
	
	private bool ExecutingInEditor()
	{
		if ( !Game.IsEditor )
		{
			return false;
		}
		
		var type = GetType();
		return typeof(ExecuteInEditor).IsAssignableFrom( type );
	}
}
using Sandbox;
using System;

[Hide]
internal sealed class Dummy : Component, Component.ExecuteInEditor
{
  
}
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Sandbox;

namespace Iconify;

/// <summary>
/// Fetches icons from the Iconify API and caches them as textures.
/// Handles disk cache and in-memory cache with proper null safety.
/// </summary>
public static class IconCache
{
	private static readonly Dictionary<string, Texture> MemoryCache = new();
	private static BaseFileSystem? _diskCache;
	private static bool _diskCacheReady;

	private static BaseFileSystem DiskCache
	{
		get
		{
			if ( !_diskCacheReady )
			{
				_diskCacheReady = true;
				try
				{
					if ( FileSystem.Data is not null )
					{
						FileSystem.Data.CreateDirectory( "iconify_cache" );
						_diskCache = FileSystem.Data.CreateSubSystem( "iconify_cache" );
					}
				}
				catch ( Exception e )
				{
					Log.Warning( $"[Iconify] Could not create disk cache: {e.Message}" );
					_diskCache = null;
				}
			}
			return _diskCache;
		}
	}

	/// <summary>
	/// Get an icon texture from cache or fetch from the Iconify API.
	/// </summary>
	public static async Task<Texture?> GetOrFetch( string prefix, string name, string color, int size )
	{
		var cacheKey = $"{prefix}_{name}_{color}_{size}";

		// Memory cache
		if ( MemoryCache.TryGetValue( cacheKey, out var cached ) && cached.IsValid() )
			return cached;

		// Disk cache
		var diskPath = $"{cacheKey}.svg";
		if ( DiskCache is not null && DiskCache.FileExists( diskPath ) )
		{
			try
			{
				var svgString = DiskCache.ReadAllText( diskPath );
				var tex = Texture.CreateFromSvgSource( svgString, size, size, null );
				if ( tex is not null && tex.IsValid() )
				{
					MemoryCache[cacheKey] = tex;
					return tex;
				}
			}
			catch { /* disk cache corrupted, re-fetch */ }
		}

		// Fetch from API
		var texture = await FetchFromApi( prefix, name, color, size, cacheKey );
		if ( texture is not null )
		{
			MemoryCache[cacheKey] = texture;
		}

		return texture;
	}

	private static async Task<Texture?> FetchFromApi( string prefix, string name, string color, int size, string cacheKey )
	{
		var encodedColor = Uri.EscapeDataString( color ?? "white" );
		var url = $"https://api.iconify.design/{prefix}/{name}.svg?color={encodedColor}&width={size}&height={size}";

		try
		{
			var svgString = await Http.RequestStringAsync( url );

			if ( string.IsNullOrEmpty( svgString ) )
			{
				Log.Warning( $"[Iconify] Empty response for {prefix}:{name}" );
				return null;
			}

			// Create texture directly from SVG string with size
			var texture = Texture.CreateFromSvgSource( svgString, size, size, null );

			if ( texture is not null && texture.IsValid() )
			{
				SaveToDiskCache( $"{cacheKey}.svg", System.Text.Encoding.UTF8.GetBytes( svgString ) );
			}

			return texture;
		}
		catch ( Exception e )
		{
			Log.Warning( $"[Iconify] API fetch failed for {prefix}:{name}: {e.Message}" );
			return null;
		}
	}

	private static void SaveToDiskCache( string path, byte[] data )
	{
		try
		{
			DiskCache?.WriteAllBytes( path, data );
		}
		catch { /* non-critical, just won't be cached */ }
	}
}
// Generated by Haxe 4.3.7

#pragma warning disable 109, 114, 219, 429, 168, 162
public class IntIterator : global::haxe.lang.HxObject {
	
	public IntIterator(global::haxe.lang.EmptyObject empty) {
	}
	
	
	public IntIterator(int min, int max) {
		global::IntIterator.__hx_ctor__IntIterator(this, min, max);
	}
	
	
	protected static void __hx_ctor__IntIterator(global::IntIterator __hx_this, int min, int max) {
		__hx_this.min = min;
		__hx_this.max = max;
	}
	
	
	public int min;
	
	public int max;
	
	public bool hasNext() {
		return ( this.min < this.max );
	}
	
	
	public int next() {
		return this.min++;
	}
	
	
	public override double __hx_setField_f(string field, int hash, double @value, bool handleProperties) {
		unchecked {
			switch (hash) {
				case 5442212:
				{
					this.max = ((int) (@value) );
					return @value;
				}
				
				
				case 5443986:
				{
					this.min = ((int) (@value) );
					return @value;
				}
				
				
				default:
				{
					return base.__hx_setField_f(field, hash, @value, handleProperties);
				}
				
			}
			
		}
	}
	
	
	public override object __hx_setField(string field, int hash, object @value, bool handleProperties) {
		unchecked {
			switch (hash) {
				case 5442212:
				{
					this.max = ((int) (global::haxe.lang.Runtime.toInt(@value)) );
					return @value;
				}
				
				
				case 5443986:
				{
					this.min = ((int) (global::haxe.lang.Runtime.toInt(@value)) );
					return @value;
				}
				
				
				default:
				{
					return base.__hx_setField(field, hash, @value, handleProperties);
				}
				
			}
			
		}
	}
	
	
	public override object __hx_getField(string field, int hash, bool throwErrors, bool isCheck, bool handleProperties) {
		unchecked {
			switch (hash) {
				case 1224901875:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "next", 1224901875)) );
				}
				
				
				case 407283053:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "hasNext", 407283053)) );
				}
				
				
				case 5442212:
				{
					return this.max;
				}
				
				
				case 5443986:
				{
					return this.min;
				}
				
				
				default:
				{
					return base.__hx_getField(field, hash, throwErrors, isCheck, handleProperties);
				}
				
			}
			
		}
	}
	
	
	public override double __hx_getField_f(string field, int hash, bool throwErrors, bool handleProperties) {
		unchecked {
			switch (hash) {
				case 5442212:
				{
					return ((double) (this.max) );
				}
				
				
				case 5443986:
				{
					return ((double) (this.min) );
				}
				
				
				default:
				{
					return base.__hx_getField_f(field, hash, throwErrors, handleProperties);
				}
				
			}
			
		}
	}
	
	
	public override object __hx_invokeField(string field, int hash, object[] dynargs) {
		unchecked {
			switch (hash) {
				case 1224901875:
				{
					return this.next();
				}
				
				
				case 407283053:
				{
					return this.hasNext();
				}
				
				
				default:
				{
					return base.__hx_invokeField(field, hash, dynargs);
				}
				
			}
			
		}
	}
	
	
	public override void __hx_getFields(global::Array<string> baseArr) {
		baseArr.push("max");
		baseArr.push("min");
		base.__hx_getFields(baseArr);
	}
	
	
}


// Generated by Haxe 4.3.7

#pragma warning disable 109, 114, 219, 429, 168, 162
public sealed class EReg : global::haxe.lang.HxObject {
	
	public EReg(global::haxe.lang.EmptyObject empty) {
	}
	
	
	public EReg(string r, string opt) {
		global::EReg.__hx_ctor__EReg(this, r, opt);
	}
	
	
	private static void __hx_ctor__EReg(global::EReg __hx_this, string r, string opt) {
		unchecked {
			int opts = ((int) (global::haxe.lang.Runtime.toInt(((object) (global::System.Text.RegularExpressions.RegexOptions.CultureInvariant) ))) );
			{
				int _g = 0;
				int _g1 = opt.Length;
				while (( _g < _g1 )) {
					int i = _g++;
					switch (((int) (opt[i]) )) {
						case 99:
						{
							opts |= ((int) (global::haxe.lang.Runtime.toInt(((object) (global::System.Text.RegularExpressions.RegexOptions.Compiled) ))) );
							break;
						}
						
						
						case 103:
						{
							__hx_this.isGlobal = true;
							break;
						}
						
						
						case 105:
						{
							opts |= ((int) (global::haxe.lang.Runtime.toInt(((object) (global::System.Text.RegularExpressions.RegexOptions.IgnoreCase) ))) );
							break;
						}
						
						
						case 109:
						{
							opts |= ((int) (global::haxe.lang.Runtime.toInt(((object) (global::System.Text.RegularExpressions.RegexOptions.Multiline) ))) );
							break;
						}
						
						
					}
					
				}
				
			}
			
			__hx_this.regex = new global::System.Text.RegularExpressions.Regex(((string) (r) ), ((global::System.Text.RegularExpressions.RegexOptions) (((object) (opts) )) ));
		}
	}
	
	
	public static string escape(string s) {
		return global::System.Text.RegularExpressions.Regex.Escape(((string) (s) ));
	}
	
	
	public global::System.Text.RegularExpressions.Regex regex;
	
	public global::System.Text.RegularExpressions.Match m;
	
	public bool isGlobal;
	
	public string cur;
	
	public bool match(string s) {
		this.m = this.regex.Match(((string) (s) ));
		this.cur = s;
		return ( this.m as global::System.Text.RegularExpressions.Group ).Success;
	}
	
	
	public string matched(int n) {
		if (( ( this.m == null ) || ( ((uint) (n) ) > this.m.Groups.Count ) )) {
			throw ((global::System.Exception) (global::haxe.Exception.thrown("EReg::matched")) );
		}
		
		if ( ! (this.m.Groups[n].Success) ) {
			return null;
		}
		
		return ( this.m.Groups[n] as global::System.Text.RegularExpressions.Capture ).Value;
	}
	
	
	public string matchedLeft() {
		return this.cur.Substring(((int) (0) ), ((int) (( this.m as global::System.Text.RegularExpressions.Capture ).Index) ));
	}
	
	
	public string matchedRight() {
		return this.cur.Substring(((int) (( ( this.m as global::System.Text.RegularExpressions.Capture ).Index + ( this.m as global::System.Text.RegularExpressions.Capture ).Length )) ));
	}
	
	
	public object matchedPos() {
		int tmp = ( this.m as global::System.Text.RegularExpressions.Capture ).Index;
		{
			int __temp_odecl1 = ( this.m as global::System.Text.RegularExpressions.Capture ).Length;
			return new global::haxe.lang.DynamicObject(new int[]{}, new object[]{}, new int[]{5393365, 5594516}, new double[]{((double) (__temp_odecl1) ), ((double) (tmp) )});
		}
		
	}
	
	
	public bool matchSub(string s, int pos, global::haxe.lang.Null<int> len) {
		unchecked {
			int len1 = ( ( ! (len.hasValue) ) ? (-1) : ((len).@value) );
			this.m = ( (( len1 < 0 )) ? (this.regex.Match(((string) (s) ), ((int) (pos) ))) : (this.regex.Match(((string) (s) ), ((int) (pos) ), ((int) (len1) ))) );
			this.cur = s;
			return ( this.m as global::System.Text.RegularExpressions.Group ).Success;
		}
	}
	
	
	public global::Array<string> split(string s) {
		if (this.isGlobal) {
			return global::cs.Lib.array<string>(((string[]) (this.regex.Split(((string) (s) ))) ));
		}
		
		global::System.Text.RegularExpressions.Match m = this.regex.Match(((string) (s) ));
		if ( ! (( m as global::System.Text.RegularExpressions.Group ).Success) ) {
			return new global::Array<string>(new string[]{s});
		}
		
		return new global::Array<string>(new string[]{s.Substring(((int) (0) ), ((int) (( m as global::System.Text.RegularExpressions.Capture ).Index) )), s.Substring(((int) (( ( m as global::System.Text.RegularExpressions.Capture ).Index + ( m as global::System.Text.RegularExpressions.Capture ).Length )) ))});
	}
	
	
	public int start(int @group) {
		return ( this.m.Groups[@group] as global::System.Text.RegularExpressions.Capture ).Index;
	}
	
	
	public int len(int @group) {
		return ( this.m.Groups[@group] as global::System.Text.RegularExpressions.Capture ).Length;
	}
	
	
	public string replace(string s, string @by) {
		unchecked {
			if (this.isGlobal) {
				return this.regex.Replace(((string) (s) ), ((string) (@by) ));
			}
			else {
				return this.regex.Replace(((string) (s) ), ((string) (@by) ), ((int) (1) ));
			}
			
		}
	}
	
	
	public string map(string s, global::haxe.lang.Function f) {
		unchecked {
			int offset = 0;
			global::StringBuf buf = new global::StringBuf();
			do {
				if (( offset >= s.Length )) {
					break;
				}
				else if ( ! (this.matchSub(s, offset, default(global::haxe.lang.Null<int>))) ) {
					buf.@add<string>(((string) (global::haxe.lang.StringExt.substr(s, offset, default(global::haxe.lang.Null<int>))) ));
					break;
				}
				
				object p = this.matchedPos();
				buf.@add<string>(((string) (global::haxe.lang.StringExt.substr(s, offset, new global::haxe.lang.Null<int>(( ((int) (global::haxe.lang.Runtime.getField_f(p, "pos", 5594516, true)) ) - ((int) (offset) ) ), true))) ));
				buf.@add<string>(global::haxe.lang.Runtime.toString(f.__hx_invoke1_o(default(double), this)));
				if (( ((int) (global::haxe.lang.Runtime.getField_f(p, "len", 5393365, true)) ) == 0 )) {
					buf.@add<string>(((string) (global::haxe.lang.StringExt.substr(s, ((int) (global::haxe.lang.Runtime.getField_f(p, "pos", 5594516, true)) ), new global::haxe.lang.Null<int>(1, true))) ));
					offset = ( ((int) (global::haxe.lang.Runtime.getField_f(p, "pos", 5594516, true)) ) + 1 );
				}
				else {
					offset = ( ((int) (global::haxe.lang.Runtime.getField_f(p, "pos", 5594516, true)) ) + ((int) (global::haxe.lang.Runtime.getField_f(p, "len", 5393365, true)) ) );
				}
				
			}
			while (this.isGlobal);
			if (( (  ! (this.isGlobal)  && ( offset > 0 ) ) && ( offset < s.Length ) )) {
				buf.@add<string>(((string) (global::haxe.lang.StringExt.substr(s, offset, default(global::haxe.lang.Null<int>))) ));
			}
			
			return buf.toString();
		}
	}
	
	
	public override object __hx_setField(string field, int hash, object @value, bool handleProperties) {
		unchecked {
			switch (hash) {
				case 4949376:
				{
					this.cur = global::haxe.lang.Runtime.toString(@value);
					return @value;
				}
				
				
				case 1821933:
				{
					this.isGlobal = global::haxe.lang.Runtime.toBool(@value);
					return @value;
				}
				
				
				case 109:
				{
					this.m = ((global::System.Text.RegularExpressions.Match) (@value) );
					return @value;
				}
				
				
				case 1723805383:
				{
					this.regex = ((global::System.Text.RegularExpressions.Regex) (@value) );
					return @value;
				}
				
				
				default:
				{
					return base.__hx_setField(field, hash, @value, handleProperties);
				}
				
			}
			
		}
	}
	
	
	public override object __hx_getField(string field, int hash, bool throwErrors, bool isCheck, bool handleProperties) {
		unchecked {
			switch (hash) {
				case 5442204:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "map", 5442204)) );
				}
				
				
				case 724060212:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "replace", 724060212)) );
				}
				
				
				case 5393365:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "len", 5393365)) );
				}
				
				
				case 67859554:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "start", 67859554)) );
				}
				
				
				case 24046298:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "split", 24046298)) );
				}
				
				
				case 1126920507:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "matchSub", 1126920507)) );
				}
				
				
				case 1271070480:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "matchedPos", 1271070480)) );
				}
				
				
				case 614073432:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "matchedRight", 614073432)) );
				}
				
				
				case 2083500811:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "matchedLeft", 2083500811)) );
				}
				
				
				case 159136996:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "matched", 159136996)) );
				}
				
				
				case 52644165:
				{
					return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "match", 52644165)) );
				}
				
				
				case 4949376:
				{
					return this.cur;
				}
				
				
				case 1821933:
				{
					return this.isGlobal;
				}
				
				
				case 109:
				{
					return this.m;
				}
				
				
				case 1723805383:
				{
					return this.regex;
				}
				
				
				default:
				{
					return base.__hx_getField(field, hash, throwErrors, isCheck, handleProperties);
				}
				
			}
			
		}
	}
	
	
	public override object __hx_invokeField(string field, int hash, object[] dynargs) {
		unchecked {
			switch (hash) {
				case 5442204:
				{
					return this.map(global::haxe.lang.Runtime.toString(dynargs[0]), ((global::haxe.lang.Function) (dynargs[1]) ));
				}
				
				
				case 724060212:
				{
					return this.replace(global::haxe.lang.Runtime.toString(dynargs[0]), global::haxe.lang.Runtime.toString(dynargs[1]));
				}
				
				
				case 5393365:
				{
					return this.len(((int) (global::haxe.lang.Runtime.toInt(dynargs[0])) ));
				}
				
				
				case 67859554:
				{
					return this.start(((int) (global::haxe.lang.Runtime.toInt(dynargs[0])) ));
				}
				
				
				case 24046298:
				{
					return this.split(global::haxe.lang.Runtime.toString(dynargs[0]));
				}
				
				
				case 1126920507:
				{
					return this.matchSub(global::haxe.lang.Runtime.toString(dynargs[0]), ((int) (global::haxe.lang.Runtime.toInt(dynargs[1])) ), global::haxe.lang.Null<object>.ofDynamic<int>(( (( dynargs.Length > 2 )) ? (dynargs[2]) : (null) )));
				}
				
				
				case 1271070480:
				{
					return this.matchedPos();
				}
				
				
				case 614073432:
				{
					return this.matchedRight();
				}
				
				
				case 2083500811:
				{
					return this.matchedLeft();
				}
				
				
				case 159136996:
				{
					return this.matched(((int) (global::haxe.lang.Runtime.toInt(dynargs[0])) ));
				}
				
				
				case 52644165:
				{
					return this.match(global::haxe.lang.Runtime.toString(dynargs[0]));
				}
				
				
				default:
				{
					return base.__hx_invokeField(field, hash, dynargs);
				}
				
			}
			
		}
	}
	
	
	public override void __hx_getFields(global::Array<string> baseArr) {
		baseArr.push("cur");
		baseArr.push("isGlobal");
		baseArr.push("m");
		baseArr.push("regex");
		base.__hx_getFields(baseArr);
	}
	
	
}


// Generated by Haxe 4.3.7

#pragma warning disable 109, 114, 219, 429, 168, 162
public class Lambda : global::haxe.lang.HxObject {
	
	public Lambda(global::haxe.lang.EmptyObject empty) {
	}
	
	
	public Lambda() {
		global::Lambda.__hx_ctor__Lambda(this);
	}
	
	
	protected static void __hx_ctor__Lambda(global::Lambda __hx_this) {
	}
	
	
	public static global::Array<A> array<A>(object it) {
		global::Array<A> a = new global::Array<A>();
		{
			object i = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(i, "hasNext", 407283053, null))) {
				A i1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(i, "next", 1224901875, null));
				a.push(i1);
			}
			
		}
		
		return a;
	}
	
	
	public static global::haxe.ds.List<A> list<A>(object it) {
		global::haxe.ds.List<A> l = new global::haxe.ds.List<A>();
		{
			object i = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(i, "hasNext", 407283053, null))) {
				A i1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(i, "next", 1224901875, null));
				l.@add(i1);
			}
			
		}
		
		return l;
	}
	
	
	public static global::Array<B> map<A, B>(object it, global::haxe.lang.Function f) {
		global::Array<B> _g = new global::Array<B>(new B[]{});
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				_g.push(global::haxe.lang.Runtime.genericCast<B>(f.__hx_invoke1_o(default(double), x1)));
			}
			
		}
		
		return _g;
	}
	
	
	public static global::Array<B> mapi<A, B>(object it, global::haxe.lang.Function f) {
		int i = 0;
		global::Array<B> _g = new global::Array<B>(new B[]{});
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				_g.push(global::haxe.lang.Runtime.genericCast<B>(f.__hx_invoke2_o(((double) (i++) ), global::haxe.lang.Runtime.undefined, default(double), x1)));
			}
			
		}
		
		return _g;
	}
	
	
	public static global::Array<A> flatten<A>(object it) {
		global::Array<A> _g = new global::Array<A>(new A[]{});
		{
			object e = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(e, "hasNext", 407283053, null))) {
				object e1 = ((object) (global::haxe.lang.Runtime.callField(e, "next", 1224901875, null)) );
				{
					object x = ((object) (global::haxe.lang.Runtime.callField(e1, "iterator", 328878574, null)) );
					while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
						A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
						_g.push(x1);
					}
					
				}
				
			}
			
		}
		
		return _g;
	}
	
	
	public static global::Array<B> flatMap<A, B>(object it, global::haxe.lang.Function f) {
		return global::Lambda.flatten<B>(((object) (global::Lambda.map<A, object>(((object) (it) ), ((global::haxe.lang.Function) (f) ))) ));
	}
	
	
	public static bool has<A>(object it, A elt) {
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				if (global::haxe.lang.Runtime.eq(x1, elt)) {
					return true;
				}
				
			}
			
		}
		
		return false;
	}
	
	
	public static bool exists<A>(object it, global::haxe.lang.Function f) {
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				if (global::haxe.lang.Runtime.toBool(f.__hx_invoke1_o(default(double), x1))) {
					return true;
				}
				
			}
			
		}
		
		return false;
	}
	
	
	public static bool @foreach<A>(object it, global::haxe.lang.Function f) {
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				if ( ! (global::haxe.lang.Runtime.toBool(f.__hx_invoke1_o(default(double), x1))) ) {
					return false;
				}
				
			}
			
		}
		
		return true;
	}
	
	
	public static void iter<A>(object it, global::haxe.lang.Function f) {
		object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
		while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
			A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
			f.__hx_invoke1_o(default(double), x1);
		}
		
	}
	
	
	public static global::Array<A> filter<A>(object it, global::haxe.lang.Function f) {
		global::Array<A> _g = new global::Array<A>(new A[]{});
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				if (global::haxe.lang.Runtime.toBool(f.__hx_invoke1_o(default(double), x1))) {
					_g.push(x1);
				}
				
			}
			
		}
		
		return _g;
	}
	
	
	public static B fold<A, B>(object it, global::haxe.lang.Function f, B first) {
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				first = global::haxe.lang.Runtime.genericCast<B>(f.__hx_invoke2_o(default(double), x1, default(double), first));
			}
			
		}
		
		return first;
	}
	
	
	public static B foldi<A, B>(object it, global::haxe.lang.Function f, B first) {
		int i = 0;
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				first = global::haxe.lang.Runtime.genericCast<B>(f.__hx_invoke3_o(default(double), x1, default(double), first, ((double) (i) ), global::haxe.lang.Runtime.undefined));
				 ++ i;
			}
			
		}
		
		return first;
	}
	
	
	public static int count<A>(object it, global::haxe.lang.Function pred) {
		int n = 0;
		if (( pred == null )) {
			object _ = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(_, "hasNext", 407283053, null))) {
				A _1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(_, "next", 1224901875, null));
				 ++ n;
			}
			
		}
		else {
			object x = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				A x1 = global::haxe.lang.Runtime.genericCast<A>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				if (global::haxe.lang.Runtime.toBool(((global::haxe.lang.Function) (pred) ).__hx_invoke1_o(default(double), x1))) {
					 ++ n;
				}
				
			}
			
		}
		
		return n;
	}
	
	
	public static bool empty<T>(object it) {
		return  ! (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) ), "hasNext", 407283053, null))) ;
	}
	
	
	public static int indexOf<T>(object it, T v) {
		unchecked {
			int i = 0;
			{
				object v2 = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
				while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(v2, "hasNext", 407283053, null))) {
					T v21 = global::haxe.lang.Runtime.genericCast<T>(global::haxe.lang.Runtime.callField(v2, "next", 1224901875, null));
					if (global::haxe.lang.Runtime.eq(v, v21)) {
						return i;
					}
					
					 ++ i;
				}
				
			}
			
			return -1;
		}
	}
	
	
	public static global::haxe.lang.Null<T> find<T>(object it, global::haxe.lang.Function f) {
		{
			object v = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(v, "hasNext", 407283053, null))) {
				T v1 = global::haxe.lang.Runtime.genericCast<T>(global::haxe.lang.Runtime.callField(v, "next", 1224901875, null));
				if (global::haxe.lang.Runtime.toBool(f.__hx_invoke1_o(default(double), v1))) {
					return new global::haxe.lang.Null<T>(v1, true);
				}
				
			}
			
		}
		
		return default(global::haxe.lang.Null<T>);
	}
	
	
	public static int findIndex<T>(object it, global::haxe.lang.Function f) {
		unchecked {
			int i = 0;
			{
				object v = ((object) (global::haxe.lang.Runtime.callField(it, "iterator", 328878574, null)) );
				while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(v, "hasNext", 407283053, null))) {
					T v1 = global::haxe.lang.Runtime.genericCast<T>(global::haxe.lang.Runtime.callField(v, "next", 1224901875, null));
					if (global::haxe.lang.Runtime.toBool(f.__hx_invoke1_o(default(double), v1))) {
						return i;
					}
					
					 ++ i;
				}
				
			}
			
			return -1;
		}
	}
	
	
	public static global::Array<T> concat<T>(object a, object b) {
		global::Array<T> l = new global::Array<T>();
		{
			object x = ((object) (global::haxe.lang.Runtime.callField(a, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x, "hasNext", 407283053, null))) {
				T x1 = global::haxe.lang.Runtime.genericCast<T>(global::haxe.lang.Runtime.callField(x, "next", 1224901875, null));
				l.push(x1);
			}
			
		}
		
		{
			object x2 = ((object) (global::haxe.lang.Runtime.callField(b, "iterator", 328878574, null)) );
			while (global::haxe.lang.Runtime.toBool(global::haxe.lang.Runtime.callField(x2, "hasNext", 407283053, null))) {
				T x3 = global::haxe.lang.Runtime.genericCast<T>(global::haxe.lang.Runtime.callField(x2, "next", 1224901875, null));
				l.push(x3);
			}
			
		}
		
		return l;
	}
	
	
}


// Generated by Haxe 4.3.7

#pragma warning disable 109, 114, 219, 429, 168, 162
namespace haxe {
	public class SysTools : global::haxe.lang.HxObject {
		
		static SysTools() {
			unchecked{
				global::haxe.SysTools.winMetaCharacters = ((global::Array<int>) (new global::Array<int>(new int[]{32, 40, 41, 37, 33, 94, 34, 60, 62, 38, 124, 10, 13, 44, 59})) );
			}
		}
		
		
		public SysTools(global::haxe.lang.EmptyObject empty) {
		}
		
		
		public SysTools() {
			global::haxe.SysTools.__hx_ctor_haxe_SysTools(this);
		}
		
		
		protected static void __hx_ctor_haxe_SysTools(global::haxe.SysTools __hx_this) {
		}
		
		
		public static global::Array<int> winMetaCharacters;
		
		public static string quoteUnixArg(string argument) {
			if (( argument == "" )) {
				return "\'\'";
			}
			
			if ( ! (new global::EReg(((string) ("[^a-zA-Z0-9_@%+=:,./-]") ), ((string) ("") )).match(argument)) ) {
				return argument;
			}
			
			return global::haxe.lang.Runtime.concat(global::haxe.lang.Runtime.concat("\'", global::StringTools.replace(argument, "\'", "\'\"\'\"\'")), "\'");
		}
		
		
		public static string quoteWinArg(string argument, bool escapeMetaCharacters) {
			unchecked {
				if ( ! (new global::EReg(((string) ("^(/)?[^ \t/\\\\\"]+$") ), ((string) ("") )).match(argument)) ) {
					global::StringBuf result = new global::StringBuf();
					bool needquote = ( ( ( ( global::haxe.lang.StringExt.indexOf(argument, " ", default(global::haxe.lang.Null<int>)) != -1 ) || ( global::haxe.lang.StringExt.indexOf(argument, "\t", default(global::haxe.lang.Null<int>)) != -1 ) ) || ( argument == "" ) ) || ( global::haxe.lang.StringExt.indexOf(argument, "/", default(global::haxe.lang.Null<int>)) > 0 ) );
					if (needquote) {
						result.@add<string>(((string) ("\"") ));
					}
					
					global::StringBuf bs_buf = new global::StringBuf();
					{
						int _g = 0;
						int _g1 = argument.Length;
						while (( _g < _g1 )) {
							int i = _g++;
							{
								global::haxe.lang.Null<int> _g2 = global::haxe.lang.StringExt.charCodeAt(argument, i);
								if ( ! (_g2.hasValue) ) {
									global::haxe.lang.Null<int> c = _g2;
									{
										if (( bs_buf.get_length() > 0 )) {
											result.@add<string>(((string) (bs_buf.toString()) ));
											bs_buf = new global::StringBuf();
										}
										
										result.addChar((c).@value);
									}
									
								}
								else {
									switch (((_g2)).@value) {
										case 34:
										{
											string bs = bs_buf.toString();
											result.@add<string>(((string) (bs) ));
											result.@add<string>(((string) (bs) ));
											bs_buf = new global::StringBuf();
											result.@add<string>(((string) ("\\\"") ));
											break;
										}
										
										
										case 92:
										{
											bs_buf.@add<string>(((string) ("\\") ));
											break;
										}
										
										
										default:
										{
											global::haxe.lang.Null<int> c1 = _g2;
											{
												if (( bs_buf.get_length() > 0 )) {
													result.@add<string>(((string) (bs_buf.toString()) ));
													bs_buf = new global::StringBuf();
												}
												
												result.addChar((c1).@value);
											}
											
											break;
										}
										
									}
									
								}
								
							}
							
						}
						
					}
					
					result.@add<string>(((string) (bs_buf.toString()) ));
					if (needquote) {
						result.@add<string>(((string) (bs_buf.toString()) ));
						result.@add<string>(((string) ("\"") ));
					}
					
					argument = result.toString();
				}
				
				if (escapeMetaCharacters) {
					global::StringBuf result1 = new global::StringBuf();
					{
						int _g3 = 0;
						int _g4 = argument.Length;
						while (( _g3 < _g4 )) {
							int i1 = _g3++;
							global::haxe.lang.Null<int> c2 = global::haxe.lang.StringExt.charCodeAt(argument, i1);
							if (( global::haxe.SysTools.winMetaCharacters.indexOf((c2).@value, default(global::haxe.lang.Null<int>)) >= 0 )) {
								result1.addChar(94);
							}
							
							result1.addChar((c2).@value);
						}
						
					}
					
					return result1.toString();
				}
				else {
					return argument;
				}
				
			}
		}
		
		
	}
}


// Generated by Haxe 4.3.7

#pragma warning disable 109, 114, 219, 429, 168, 162
namespace haxe.io {
	public class Error : global::haxe.lang.Enum {
		
		protected Error(int index) : base(index) {
		}
		
		
		public static readonly global::haxe.io.Error Blocked = new global::haxe.io.Error_Blocked();
		
		public static readonly global::haxe.io.Error Overflow = new global::haxe.io.Error_Overflow();
		
		public static readonly global::haxe.io.Error OutsideBounds = new global::haxe.io.Error_OutsideBounds();
		
		public static global::haxe.io.Error Custom(object e) {
			return new global::haxe.io.Error_Custom(e);
		}
		
		
		protected static readonly string[] __hx_constructs = new string[]{"Blocked", "Overflow", "OutsideBounds", "Custom"};
		
	}
}



#pragma warning disable 109, 114, 219, 429, 168, 162
namespace haxe.io {
	public sealed class Error_Blocked : global::haxe.io.Error {
		
		public Error_Blocked() : base(0) {
		}
		
		
		public override string getTag() {
			return "Blocked";
		}
		
		
	}
}



#pragma warning disable 109, 114, 219, 429, 168, 162
namespace haxe.io {
	public sealed class Error_Overflow : global::haxe.io.Error {
		
		public Error_Overflow() : base(1) {
		}
		
		
		public override string getTag() {
			return "Overflow";
		}
		
		
	}
}



#pragma warning disable 109, 114, 219, 429, 168, 162
namespace haxe.io {
	public sealed class Error_OutsideBounds : global::haxe.io.Error {
		
		public Error_OutsideBounds() : base(2) {
		}
		
		
		public override string getTag() {
			return "OutsideBounds";
		}
		
		
	}
}



#pragma warning disable 109, 114, 219, 429, 168, 162
namespace haxe.io {
	public sealed class Error_Custom : global::haxe.io.Error {
		
		public Error_Custom(object e) : base(3) {
			this.e = e;
		}
		
		
		public override global::Array<object> getParams() {
			return new global::Array<object>(new object[]{this.e});
		}
		
		
		public override string getTag() {
			return "Custom";
		}
		
		
		public override int GetHashCode() {
			unchecked {
				return global::haxe.lang.Enum.paramsGetHashCode(3, new object[]{this.e});
			}
		}
		
		
		public override bool Equals(object other) {
			if (global::System.Object.ReferenceEquals(((object) (this) ), ((object) (other) ))) {
				return true;
			}
			
			global::haxe.io.Error_Custom en = ( other as global::haxe.io.Error_Custom );
			if (( en == null )) {
				return false;
			}
			
			if ( ! (global::Type.enumEq<object>(((object) (this.e) ), ((object) (en.e) ))) ) {
				return false;
			}
			
			return true;
		}
		
		
		public override string toString() {
			return global::haxe.lang.Enum.paramsToString("Custom", new object[]{this.e});
		}
		
		
		public readonly object e;
		
	}
}


// Generated by Haxe 4.3.7

#pragma warning disable 109, 114, 219, 429, 168, 162
namespace haxe.iterators {
	public class StringIteratorUnicode : global::haxe.lang.HxObject {
		
		public StringIteratorUnicode(global::haxe.lang.EmptyObject empty) {
		}
		
		
		public StringIteratorUnicode(string s) {
			global::haxe.iterators.StringIteratorUnicode.__hx_ctor_haxe_iterators_StringIteratorUnicode(this, s);
		}
		
		
		protected static void __hx_ctor_haxe_iterators_StringIteratorUnicode(global::haxe.iterators.StringIteratorUnicode __hx_this, string s) {
			__hx_this.offset = 0;
			{
				__hx_this.s = s;
			}
			
		}
		
		
		public static global::haxe.iterators.StringIteratorUnicode unicodeIterator(string s) {
			return new global::haxe.iterators.StringIteratorUnicode(((string) (s) ));
		}
		
		
		public int offset;
		
		public string s;
		
		public bool hasNext() {
			return ( this.offset < this.s.Length );
		}
		
		
		public int next() {
			unchecked {
				int c = global::StringTools.utf16CodePointAt(this.s, this.offset++);
				if (( c >= 65536 )) {
					this.offset++;
				}
				
				return c;
			}
		}
		
		
		public override double __hx_setField_f(string field, int hash, double @value, bool handleProperties) {
			unchecked {
				switch (hash) {
					case 1614780307:
					{
						this.offset = ((int) (@value) );
						return @value;
					}
					
					
					default:
					{
						return base.__hx_setField_f(field, hash, @value, handleProperties);
					}
					
				}
				
			}
		}
		
		
		public override object __hx_setField(string field, int hash, object @value, bool handleProperties) {
			unchecked {
				switch (hash) {
					case 115:
					{
						this.s = global::haxe.lang.Runtime.toString(@value);
						return @value;
					}
					
					
					case 1614780307:
					{
						this.offset = ((int) (global::haxe.lang.Runtime.toInt(@value)) );
						return @value;
					}
					
					
					default:
					{
						return base.__hx_setField(field, hash, @value, handleProperties);
					}
					
				}
				
			}
		}
		
		
		public override object __hx_getField(string field, int hash, bool throwErrors, bool isCheck, bool handleProperties) {
			unchecked {
				switch (hash) {
					case 1224901875:
					{
						return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "next", 1224901875)) );
					}
					
					
					case 407283053:
					{
						return ((global::haxe.lang.Function) (new global::haxe.lang.Closure(this, "hasNext", 407283053)) );
					}
					
					
					case 115:
					{
						return this.s;
					}
					
					
					case 1614780307:
					{
						return this.offset;
					}
					
					
					default:
					{
						return base.__hx_getField(field, hash, throwErrors, isCheck, handleProperties);
					}
					
				}
				
			}
		}
		
		
		public override double __hx_getField_f(string field, int hash, bool throwErrors, bool handleProperties) {
			unchecked {
				switch (hash) {
					case 1614780307:
					{
						return ((double) (this.offset) );
					}
					
					
					default:
					{
						return base.__hx_getField_f(field, hash, throwErrors, handleProperties);
					}
					
				}
				
			}
		}
		
		
		public override object __hx_invokeField(string field, int hash, object[] dynargs) {
			unchecked {
				switch (hash) {
					case 1224901875:
					{
						return this.next();
					}
					
					
					case 407283053:
					{
						return this.hasNext();
					}
					
					
					default:
					{
						return base.__hx_invokeField(field, hash, dynargs);
					}
					
				}
				
			}
		}
		
		
		public override void __hx_getFields(global::Array<string> baseArr) {
			baseArr.push("s");
			baseArr.push("offset");
			base.__hx_getFields(baseArr);
		}
		
		
	}
}


// Generated by Haxe 4.3.7

#pragma warning disable 109, 114, 219, 429, 168, 162
namespace sys.thread {
	public class NoEventLoopException : global::haxe.Exception {
		
		public NoEventLoopException(global::haxe.lang.EmptyObject empty) : base(global::haxe.lang.EmptyObject.EMPTY) {
		}
		
		
		public NoEventLoopException(string msg, global::haxe.Exception previous) : base(((string) (( (( msg == null )) ? ("Event loop is not available. Refer to sys.thread.Thread.runWithEventLoop.") : (msg) )) ), ((global::haxe.Exception) (( (( previous == null )) ? (null) : (previous) )) ), default(object)) {
			{
				if (( msg == null )) {
					msg = "Event loop is not available. Refer to sys.thread.Thread.runWithEventLoop.";
				}
				
			}
			
			this.__shiftStack();
		}
		
		
	}
}


#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Sandbox.git.models;

namespace Sandbox.git;

/// <summary>
/// Branch-related git operations (create, list, rename, delete, query).
/// </summary>
public static class Branch {
	/// <summary>
	/// Create a new branch from the given start point.
	/// </summary>
	/// <param name="repository">The repository in which to create the new branch.</param>
	/// <param name="name">The name of the new branch.</param>
	/// <param name="startPoint">A committish string that the new branch should be based on, or null if the branch should be created from the current HEAD.</param>
	/// <param name="noTrack">If true, do not set up tracking (e.g. when branching from a remote branch).</param>
	public static async Task CreateBranchAsync(
		Repository repository,
		string name,
		string? startPoint,
		bool noTrack = false) {
		if ( repository == null )
			throw new ArgumentNullException(nameof(repository));
		if ( string.IsNullOrEmpty(name) )
			throw new ArgumentException("Branch name is required.", nameof(name));

		var args = GetCreateBranchArgs(name, startPoint, noTrack);
		await Core.GitAsync(args, repository.Path, "createBranch").ConfigureAwait(false);
	}

	/// <summary>
	/// Gets the short names of all local branches.
	/// </summary>
	public static async Task<string[]> GetBranchNamesAsync(Repository repository) {
		if ( repository == null )
			throw new ArgumentNullException(nameof(repository));

		var result = await Core.GitAsync(
			GetBranchNamesArgs(),
			repository.Path,
			"getBranchNames").ConfigureAwait(false);

		return ParseBranchLines(result.Stdout)
			.Select(line => line.Trim())
			.Where(s => s.Length > 0)
			.ToArray();
	}

	/// <summary>
	/// Rename the given branch to a new name.
	/// </summary>
	/// <param name="repository">The repository.</param>
	/// <param name="branch">The branch to rename.</param>
	/// <param name="newName">The new name.</param>
	/// <param name="force">If true, use -M to force rename (e.g. case-only renames on case-insensitive filesystems).</param>
	public static async Task RenameBranchAsync(
		Repository repository,
		models.Branch branch,
		string newName,
		bool? force = null) {
		if ( repository == null )
			throw new ArgumentNullException(nameof(repository));
		if ( branch == null )
			throw new ArgumentNullException(nameof(branch));
		if ( string.IsNullOrEmpty(newName) )
			throw new ArgumentException("New branch name is required.", nameof(newName));

		try {
			await Core.GitAsync(
				GetRenameBranchArgs(branch.NameWithoutRemote, newName, force),
				repository.Path,
				"renameBranch").ConfigureAwait(false);
		} catch ( GitException ex ) {
			// If we failed and the branch name only differs by case, retry with -M (see Desktop #21320).
			if ( force != null )
				throw;

			if ( !IsBranchAlreadyExistsError(ex.Result) )
				throw;

			var m = Regex.Match(ex.Result.Stderr, @"fatal: a branch named '(.+?)' already exists");
			if ( !m.Success || !string.Equals(m.Groups[1].Value, newName, StringComparison.OrdinalIgnoreCase) )
				throw;

			var names = await GetBranchNamesAsync(repository).ConfigureAwait(false);
			if ( names.Contains(newName) )
				throw;

			await RenameBranchAsync(repository, branch, newName, true).ConfigureAwait(false);
		}
	}

	/// <summary>
	/// Delete the branch locally (force delete with -D).
	/// </summary>
	public static async Task DeleteLocalBranchAsync(Repository repository, string branchName) {
		if ( repository == null )
			throw new ArgumentNullException(nameof(repository));
		if ( string.IsNullOrEmpty(branchName) )
			throw new ArgumentException("Branch name is required.", nameof(branchName));

		await Core.GitAsync(
			GetDeleteLocalBranchArgs(branchName),
			repository.Path,
			"deleteLocalBranch").ConfigureAwait(false);
	}

	/// <summary>
	/// Deletes a remote branch (push :ref to remote). If the remote ref was already deleted, removes the local remote-tracking ref.
	/// </summary>
	/// <param name="repository">The repository.</param>
	/// <param name="remote">The remote (e.g. origin).</param>
	/// <param name="remoteBranchName">The name of the branch on the remote.</param>
	public static async Task DeleteRemoteBranchAsync(
		Repository repository,
		IRemote remote,
		string remoteBranchName) {
		if ( repository == null )
			throw new ArgumentNullException(nameof(repository));
		if ( remote == null )
			throw new ArgumentNullException(nameof(remote));
		if ( string.IsNullOrEmpty(remoteBranchName) )
			throw new ArgumentException("Remote branch name is required.", nameof(remoteBranchName));

		var args = GetDeleteRemoteBranchArgs(remote.Name, remoteBranchName);
		var result = await Core.GitAsync(
			args,
			repository.Path,
			"deleteRemoteBranch",
			successExitCodes: new HashSet<int> { 0, 1 }).ConfigureAwait(false);

		// If push failed (e.g. ref already deleted on remote), remove our local remote-tracking ref.
		if ( result.ExitCode != 0 ) {
			var refName = $"refs/remotes/{remote.Name}/{remoteBranchName}";
			await DeleteRefAsync(repository, refName).ConfigureAwait(false);
		}
	}

	/// <summary>
	/// Finds branches whose tip equals the given committish (sha, HEAD, etc.).
	/// </summary>
	/// <returns>List of branch short names, or null if the committish could not be resolved or was malformed.</returns>
	public static async Task<string[]?> GetBranchesPointedAtAsync(Repository repository, string commitish) {
		if ( repository == null )
			throw new ArgumentNullException(nameof(repository));
		if ( string.IsNullOrEmpty(commitish) )
			throw new ArgumentException("Committish is required.", nameof(commitish));

		var result = await Core.GitAsync(
			GetBranchesPointedAtArgs(commitish),
			repository.Path,
			"branchPointedAt",
			successExitCodes: new HashSet<int> { 0, 1, 129 }).ConfigureAwait(false);

		if ( result.ExitCode == 1 || result.ExitCode == 129 )
			return null;

		var lines = result.Stdout.Split('\n');
		return lines.Length > 0 && string.IsNullOrEmpty(lines[lines.Length - 1])
			? lines.Take(lines.Length - 1).ToArray()
			: lines;
	}

	/// <summary>
	/// Gets all branches that have been merged into the given branch.
	/// </summary>
	/// <param name="repository">The repository.</param>
	/// <param name="branchName">The base branch name (e.g. main).</param>
	/// <returns>Map of branch canonical ref to its tip sha (excluding the base branch itself).</returns>
	public static async Task<Dictionary<string, string>> GetMergedBranchesAsync(
		Repository repository,
		string branchName) {
		if ( repository == null )
			throw new ArgumentNullException(nameof(repository));
		if ( string.IsNullOrEmpty(branchName) )
			throw new ArgumentException("Branch name is required.", nameof(branchName));

		var canonicalBranchRef = FormatAsLocalRef(branchName);
		var result = await Core.GitAsync(
			GetMergedBranchesArgs(branchName),
			repository.Path,
			"mergedBranches").ConfigureAwait(false);

		var merged = new Dictionary<string, string>(StringComparer.Ordinal);
		foreach ( var line in ParseBranchLines(result.Stdout) ) {
			var trimmed = line.Trim();
			if ( trimmed.Length == 0 )
				continue;

			var firstSpace = trimmed.IndexOf(' ');
			if ( firstSpace <= 0 )
				continue;

			var sha = trimmed.Substring(0, firstSpace);
			var canonicalRef = trimmed.Substring(firstSpace + 1).Trim();
			if ( canonicalRef == canonicalBranchRef )
				continue;

			merged[canonicalRef] = sha;
		}

		return merged;
	}

	// --- Helpers ---

	/// <summary>
	/// Canonical local branch ref (e.g. refs/heads/main).
	/// </summary>
	public static string FormatAsLocalRef(string branchName) {
		if ( string.IsNullOrEmpty(branchName) )
			throw new ArgumentException("Branch name is required.", nameof(branchName));
		return "refs/heads/" + branchName;
	}

	static async Task DeleteRefAsync(Repository repository, string refName) {
		await Core.GitAsync(
			GetDeleteRefArgs(refName),
			repository.Path,
			"deleteRef",
			successExitCodes: new HashSet<int> { 0, 1 }).ConfigureAwait(false);
	}

	// --- Args (exposed for testing) ---

	/// <summary>Builds git arguments for creating a branch. Exposed for testing.</summary>
	public static string[] GetCreateBranchArgs(string name, string? startPoint, bool noTrack = false) {
		if ( string.IsNullOrEmpty(name) )
			throw new ArgumentException("Branch name is required.", nameof(name));
		var args = startPoint != null
			? new[] { "branch", name, startPoint }
			: new[] { "branch", name };
		if ( noTrack )
			args = args.Concat(new[] { "--no-track" }).ToArray();
		return args;
	}

	/// <summary>Builds git arguments for listing branch names. Exposed for testing.</summary>
	public static string[] GetBranchNamesArgs() {
		return new[] { "branch", "--format=%(refname:short)" };
	}

	/// <summary>Builds git arguments for renaming a branch. Exposed for testing.</summary>
	public static string[] GetRenameBranchArgs(string nameWithoutRemote, string newName, bool? force = null) {
		if ( string.IsNullOrEmpty(nameWithoutRemote) )
			throw new ArgumentException("Branch name is required.", nameof(nameWithoutRemote));
		if ( string.IsNullOrEmpty(newName) )
			throw new ArgumentException("New branch name is required.", nameof(newName));
		return new[] { "branch", force == true ? "-M" : "-m", nameWithoutRemote, newName };
	}

	/// <summary>Builds git arguments for deleting a local branch. Exposed for testing.</summary>
	public static string[] GetDeleteLocalBranchArgs(string branchName) {
		if ( string.IsNullOrEmpty(branchName) )
			throw new ArgumentException("Branch name is required.", nameof(branchName));
		return new[] { "branch", "-D", branchName };
	}

	/// <summary>Builds git arguments for deleting a remote branch (push :ref). Exposed for testing.</summary>
	public static string[] GetDeleteRemoteBranchArgs(string remoteName, string remoteBranchName) {
		if ( string.IsNullOrEmpty(remoteName) )
			throw new ArgumentException("Remote name is required.", nameof(remoteName));
		if ( string.IsNullOrEmpty(remoteBranchName) )
			throw new ArgumentException("Remote branch name is required.", nameof(remoteBranchName));
		return new[] { "push", remoteName, $":{remoteBranchName}" };
	}

	/// <summary>Builds git arguments for listing branches pointed at a committish. Exposed for testing.</summary>
	public static string[] GetBranchesPointedAtArgs(string commitish) {
		if ( string.IsNullOrEmpty(commitish) )
			throw new ArgumentException("Committish is required.", nameof(commitish));
		return new[] { "branch", $"--points-at={commitish}", "--format=%(refname:short)" };
	}

	/// <summary>Builds git arguments for listing merged branches. Exposed for testing.</summary>
	public static string[] GetMergedBranchesArgs(string branchName) {
		if ( string.IsNullOrEmpty(branchName) )
			throw new ArgumentException("Branch name is required.", nameof(branchName));
		return new[] { "branch", "--format=%(objectname) %(refname)", "--merged", branchName };
	}

	/// <summary>Builds git arguments for deleting a ref. Exposed for testing.</summary>
	public static string[] GetDeleteRefArgs(string refName) {
		if ( string.IsNullOrEmpty(refName) )
			throw new ArgumentException("Ref name is required.", nameof(refName));
		return new[] { "update-ref", "-d", refName };
	}

	static bool IsBranchAlreadyExistsError(GitResult result) {
		return result.ExitCode != 0
		       && (result.Stderr?.Contains("a branch named ", StringComparison.OrdinalIgnoreCase) == true
		           && result.Stderr?.Contains(" already exists", StringComparison.OrdinalIgnoreCase) == true);
	}

	static IEnumerable<string> ParseBranchLines(string stdout) {
		if ( string.IsNullOrEmpty(stdout) )
			yield break;
		foreach ( var line in stdout.Split('\n') )
			yield return line;
	}
}
using System.Collections.Generic;

namespace Sandbox.git.models;

/// <summary>
/// The result of comparing two refs in a repository.
/// </summary>
public interface ICompareResult : IAheadBehind {
	IReadOnlyList<Commit> Commits { get; }
}
using System;
using System.Collections.Generic;

namespace Sandbox.git.models;

/// <summary>
/// A parsed trailer from a commit message (e.g. Co-Authored-By: Name &lt;email&gt;).
/// </summary>
public interface ITrailer {
	string Token { get; }
	string Value { get; }
}

/// <summary>
/// Helpers for interpreting commit trailers.
/// </summary>
public static class TrailerHelpers {
	public const string CoAuthoredByToken = "Co-Authored-By";

	public static bool IsCoAuthoredByTrailer(ITrailer trailer) {
		return trailer != null &&
		       string.Equals(trailer.Token, CoAuthoredByToken, StringComparison.OrdinalIgnoreCase);
	}

	/// <summary>
	/// Parses unfolded trailer lines (e.g. from %(trailers:unfold,only)) with the given key-value separator.
	/// Each line is "Token: Value" or "Token separator Value".
	/// </summary>
	public static IReadOnlyList<ITrailer> ParseRawUnfoldedTrailers(string text, char keyValueSeparator = ':') {
		if ( string.IsNullOrWhiteSpace(text) )
			return Array.Empty<ITrailer>();

		var list = new List<ITrailer>();
		foreach ( var line in text.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries) ) {
			var trimmed = line.Trim();
			if ( trimmed.Length == 0 )
				continue;

			var sepIndex = trimmed.IndexOf(keyValueSeparator);
			if ( sepIndex <= 0 )
				continue;

			var token = trimmed.Substring(0, sepIndex).Trim();
			var value = trimmed.Substring(sepIndex + 1).Trim();
			if ( token.Length > 0 )
				list.Add(new Trailer(token, value));
		}

		return list;
	}
}
namespace Sandbox.git.models;

/// <summary>
/// Base type for a directory you can run git commands in.
/// </summary>
public class WorkingTree {
	public string Path { get; }

	public WorkingTree(string path) {
		Path = path ?? string.Empty;
	}
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "igor" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "igor.reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-03-29 03:35:54" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "25" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.110.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.110.0")]
#if JETBRAINS_ANNOTATIONS
#endif

namespace Sandbox.Reactivity;

/// <summary>
/// An object that contains a reactive value. Reading the value inside an effect will cause it to re-run when it
/// changes.
/// </summary>
/// <typeparam name="T">The type of value this object contains.</typeparam>
/// <remarks>
/// This can be used to abstract over a <see cref="State{T}" /> or <see cref="Derived{T}" /> as needed.
/// </remarks>
#if JETBRAINS_ANNOTATIONS
#endif
public interface IState<T>
{
	/// <summary>
	/// The current value.
	/// </summary>
	T Value { get; set; }
}

/// <inheritdoc cref="IState{T}" />
#if JETBRAINS_ANNOTATIONS
#endif
public interface IReadOnlyState<out T>
{
	/// <inheritdoc cref="IState{T}.Value" />
	T Value { get; }
}
#if SANDBOX
namespace Sandbox.Reactivity.Internals;

/// <summary>
/// Maintains a list of types that are assignable to the given type.
/// </summary>
internal static class TypeHierarchy<T>
{
	/// <summary>
	/// All types that are assignable to <typeparamref name="T"/>.
	/// </summary>
	[SkipHotload]
	// ReSharper disable once StaticMemberInGenericType
	public static readonly IEnumerable<Type> Types;

	static TypeHierarchy()
	{
		// since this is most likely going to be used for simple event types, we're going to assume that the hierarchy
		// won't be very large and that checking a list would be faster than hashing for a set
		var next = typeof(T);
		var hierarchy = new List<Type>();

		while (next != null)
		{
			hierarchy.Add(next);

			foreach (var type in next.GetInterfaces())
			{
				if (!hierarchy.Contains(type))
				{
					hierarchy.Add(type);
				}
			}

			next = next.BaseType;

			if (next == typeof(object))
			{
				break;
			}
		}

		Types = hierarchy;
	}
}
#endif
#if SANDBOX
using Sandbox.Reactivity.Internals;
#if JETBRAINS_ANNOTATIONS
using JetBrains.Annotations;
#endif

// we can't wrap the BuildRenderTree method for razor components, so we need something that can set up the proper
// scope inside the markup itself

namespace Sandbox.Reactivity;

/// <summary>
/// A disposable that's used to enable reactivity for a <see cref="ReactivePanelComponent" /> or
/// <see cref="ReactivePanel" /> during rendering.
/// </summary>
#if JETBRAINS_ANNOTATIONS
[PublicAPI]
#endif
public readonly ref struct ReactivePanelScope : IDisposable
{
	private readonly Effect.ExecutionScope _executionScope;

	internal ReactivePanelScope(IReactivePanel panel)
	{
		if (panel.RenderEffectRoot is { } previousRoot)
		{
			// don't teardown previous root since we're already building the render tree by this point
			previousRoot.Dispose(false);
		}

		// nested panels don't render immediately when a containing panel's tree is rendering, so the parent is
		// always null anyway
		var effectRoot = new Effect(null, null, true, () => panel.Version++);
		effectRoot.SetDebugInfo(panel.GetType().ToSimpleString(false) + " (Render)",
			panel is ReactivePanel ? "view_quilt" : "monitor",
			new CallLocation(2),
			panel is ReactivePanel reactive ? reactive.GameObject?.GetComponent<IReactivePanel>() : panel);

		panel.RenderEffectRoot = effectRoot;
		_executionScope = new Effect.ExecutionScope(effectRoot);
	}

	public void Dispose()
	{
		_executionScope.Dispose();
	}
}
#endif
#if SANDBOX
using System.Diagnostics;
using Sandbox.Reactivity.Internals;
using Sandbox.UI;
using static Sandbox.Reactivity.Reactive;
#if JETBRAINS_ANNOTATIONS
using JetBrains.Annotations;
#endif

namespace Sandbox.Reactivity;

/// <summary>
/// The reactive counterpart to <see cref="Panel" /> that allows usage of reactive properties.
/// </summary>
/// <remarks>
/// Make sure you set up an effect root using <see cref="PanelRoot" /> at the top of your razor markup:
/// <code>
/// @{ using var _ = PanelRoot(); }
/// </code>
/// Engine limitations prevent this from being done automatically.
/// </remarks>
#if JETBRAINS_ANNOTATIONS
[PublicAPI]
#endif
public class ReactivePanel : Panel, IReactivePropertyContainer, IReactivePanel
{
	private Effect? _effectRoot;

	private Effect? _renderEffectRoot;

	private int _version;

	public ReactivePanel()
	{
		var parent = Runtime.CurrentEffect;

		_effectRoot = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
			{
				OnActivate();
				return null;
			},
			parent,
			false);

		_effectRoot.SetDebugInfo(DisplayInfo.For(this).Name,
			DisplayInfo.For(this).Icon,
			new CallLocation(GetType(), nameof(OnActivate)),
			parent ?? (object?)this);

		_effectRoot.Run();
	}

	Effect? IReactivePanel.RenderEffectRoot
	{
		get => _renderEffectRoot;
		set => _renderEffectRoot = value;
	}

	int IReactivePanel.Version
	{
		get => _version;
		set => _version = value;
	}

	Dictionary<int, IProducer> IReactivePropertyContainer.Producers { get; } = [];

	protected ReactivePanelScope PanelRoot()
	{
		return new ReactivePanelScope(this);
	}

	public sealed override void Delete(bool immediate = false)
	{
		_renderEffectRoot?.Dispose();
		_renderEffectRoot = null;

		_effectRoot?.Dispose();
		_effectRoot = null;

		base.Delete(immediate);
	}

	protected sealed override int BuildHash()
	{
		return _version;
	}

	/// <summary>
	/// Called inside an effect root when this panel is instantiated, allowing for effects to be created. When this
	/// panel is deleted, the effect root (and all of its descendants) are disposed.
	/// </summary>
	protected virtual void OnActivate()
	{
	}
}
#endif
#if DEBUG

using Sandbox.Reactivity.Internals;
using Sandbox.UI;

namespace Sandbox.Reactivity.Editor.Debugger;

// ReSharper disable once ClassNeverInstantiated.Global
[Dock("Editor", "Reactivity Debugger", "local_fire_department")]
internal sealed partial class DebuggerWidget : Widget
{
	private readonly TreeView _tree;

	public DebuggerWidget(Widget? parent)
		: base(parent)
	{
		Layout = new Column
		{
			Spacing = 2,
			Children =
			[
				new Widget
				{
					Layout = new Row
					{
						Spacing = 2,
						Children =
						[
							// TODO: search bar
							new Widget
							{
								HorizontalSizeMode = SizeMode.Flexible,
							},
							new Widget
							{
								BackgroundColor = Theme.ControlBackground,
								BorderRadius = Theme.ControlRadius,
								Layout = new Row
								{
									Children =
									[
										new ToolButton("Settings", "more_vert", this)
										{
											MouseLeftPress = () => Settings<DebuggerWidget>.OpenContextMenu(this),
										},
									],
								},
							},
						],
					},
				},
				new Widget
				{
					BackgroundColor = Theme.ControlBackground,
					BorderRadius = Theme.ControlRadius,
					Layout = new Column
					{
						Children =
						[
							_tree = new TreeView
							{
								Margin = new Margin(8, 0),
								SelectionOverride = () => EditorUtility.InspectorObject,
							},
						],
					},
				},
			],
		};

		Effect.OnEffectRootCreated += OnEffectRootCreated;
	}

	public override void OnDestroyed()
	{
		foreach (var item in _tree.Items) // .Items makes a copy
		{
			if (item is EffectTreeNode node)
			{
				node.Dispose();
			}
		}

		_tree.Clear();

		Effect.OnEffectRootCreated -= OnEffectRootCreated;
	}

	private void OnEffectRootCreated(Effect root)
	{
		if (root.IsDisposed)
		{
			// just in case - effects from other scenes (e.g. prefab editors) might cause this to happen
			return;
		}

		if (!ShowUiEffects && root.Parent is IReactivePanel)
		{
			return;
		}

		if (!ShowGameplayEffects && root.Parent is (Component or GameObject) and not IReactivePanel)
		{
			return;
		}

		var node = new EffectTreeNode(root);
		_tree.AddItem(node);

		if (AutoExpand)
		{
			_tree.Open(node, true);
		}
	}
}

#endif