Search the source of every open source package.
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 <email>).
/// </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