Editor dock widget for a texture browser used in the Hammer editor. It lists material assets, shows thumbnails, filters by name/keyword/usage, previews texture sizes, and exposes actions to mark/replace/open materials and interact with the Hammer map via reflection.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Editor;
using Sandbox;
namespace HammerTextureBrowser;
[Dock( "Editor", "OG Texture Browser", "texture" )]
public sealed class HammerTextureBrowserDock : Widget
{
private static readonly Regex QuotedMaterialProperty = new( "\"(?<key>[^\"]+)\"\\s+\"(?<value>[^\"]+)\"", RegexOptions.Compiled | RegexOptions.CultureInvariant );
private static readonly string[] PrimaryTextureKeys =
[
"TextureColor",
"TextureBaseColor",
"TextureAlbedo",
"AlbedoTexture",
"BaseTexture",
"BaseColorTexture",
"g_tColor",
"g_tBaseColor",
"g_tAlbedo"
];
private readonly HammerTextureList TextureList;
private readonly ComboBox SizeCombo;
private readonly LineEdit FilterEdit;
private readonly ComboBox KeywordsCombo;
private readonly Checkbox OnlyUsedTextures;
private readonly Label SelectedTextureLabel;
private readonly Label TextureSizeLabel;
private readonly Label CountLabel;
private readonly Button MarkButton;
private readonly Button ReplaceButton;
private readonly Button ReloadButton;
private readonly Button OpenSourceButton;
private List<Asset> AllMaterials = new();
private Asset SelectedMaterial;
private string KeywordFilter = string.Empty;
private int PreviewSize = 128;
public HammerTextureBrowserDock( Widget parent ) : base( parent )
{
MinimumSize = new( 420, 320 );
WindowTitle = "OG Texture Browser";
SetWindowIcon( "texture" );
Layout = Layout.Column();
Layout.Margin = 0;
Layout.Spacing = 0;
TextureList = Layout.Add( new HammerTextureList( this ), 1 );
TextureList.OnTextureSelected = SelectMaterial;
TextureList.OnTextureActivated = SelectMaterial;
TextureList.OnOpenInEditor = OpenMaterialSource;
TextureList.OnOpenInAssetBrowser = OpenInAssetBrowser;
var bottom = Layout.Add( new Widget( this ) );
bottom.Layout = Layout.Column();
bottom.Layout.Margin = 4;
bottom.Layout.Spacing = 3;
bottom.FixedHeight = Theme.RowHeight * 2 + 12;
bottom.OnPaintOverride = () =>
{
Paint.ClearPen();
Paint.SetBrush( Theme.ControlBackground );
Paint.DrawRect( bottom.LocalRect );
return false;
};
var topRow = bottom.Layout.AddRow();
topRow.Spacing = 5;
var sizeBlock = topRow.Add( new Widget( this ) );
sizeBlock.FixedWidth = 138;
sizeBlock.MinimumWidth = 138;
sizeBlock.MaximumWidth = 138;
sizeBlock.Layout = Layout.Row();
sizeBlock.Layout.Margin = 0;
sizeBlock.Layout.Spacing = 5;
AddSmallLabel( sizeBlock.Layout, "Size:" );
SizeCombo = sizeBlock.Layout.Add( new ComboBox( sizeBlock ) );
SizeCombo.FixedWidth = 88;
SizeCombo.MinimumWidth = 88;
SizeCombo.MaximumWidth = 88;
SizeCombo.AddItem( "32x32", null, () => SetPreviewSize( 32 ) );
SizeCombo.AddItem( "64x64", null, () => SetPreviewSize( 64 ) );
SizeCombo.AddItem( "128x128", null, () => SetPreviewSize( 128 ) );
SizeCombo.AddItem( "256x256", null, () => SetPreviewSize( 256 ) );
SizeCombo.AddItem( "1:1", null, () => SetPreviewSize( 128 ) );
SizeCombo.CurrentIndex = 2;
StyleBottomInput( SizeCombo, 24 );
AddSmallLabel( topRow, "Filter:", 58 );
FilterEdit = topRow.Add( new LineEdit( this ) );
FilterEdit.FixedWidth = 176;
FilterEdit.MinimumWidth = 176;
FilterEdit.MaximumWidth = 176;
FilterEdit.PlaceholderText = "texture name";
FilterEdit.TextChanged += _ => RefreshList();
StyleBottomInput( FilterEdit );
SelectedTextureLabel = topRow.Add( new Label( this ) );
SelectedTextureLabel.MinimumWidth = 180;
SelectedTextureLabel.Alignment = TextFlag.LeftCenter;
SelectedTextureLabel.Text = "";
OpenSourceButton = topRow.Add( new Button( "Open Source" ) );
OpenSourceButton.FixedWidth = 96;
OpenSourceButton.Clicked = OpenSelectedSource;
var bottomRow = bottom.Layout.AddRow();
bottomRow.Spacing = 5;
OnlyUsedTextures = bottomRow.Add( new Checkbox( "Only used textures" ) );
OnlyUsedTextures.FixedWidth = 138;
OnlyUsedTextures.StateChanged += _ => RefreshList();
AddSmallLabel( bottomRow, "Keywords:", 58 );
KeywordsCombo = bottomRow.Add( new ComboBox( this ) );
KeywordsCombo.FixedWidth = 176;
KeywordsCombo.MinimumWidth = 176;
KeywordsCombo.MaximumWidth = 176;
KeywordsCombo.AddItem( "All Keywords", null, () => SetKeywordFilter( string.Empty ) );
StyleBottomInput( KeywordsCombo, 24 );
MarkButton = bottomRow.Add( new Button( "Mark" ) );
MarkButton.FixedWidth = 76;
MarkButton.Clicked = MarkSelectedMaterial;
ReplaceButton = bottomRow.Add( new Button( "Replace" ) );
ReplaceButton.FixedWidth = 76;
ReplaceButton.Clicked = ReplaceSelectedMaterial;
ReloadButton = bottomRow.Add( new Button( "Reload" ) );
ReloadButton.FixedWidth = 76;
ReloadButton.Clicked = Reload;
TextureSizeLabel = bottomRow.Add( new Label( this ) );
TextureSizeLabel.MinimumWidth = 0;
TextureSizeLabel.MaximumWidth = 68;
TextureSizeLabel.Alignment = TextFlag.RightCenter;
bottomRow.AddStretchCell( 1 );
CountLabel = bottomRow.Add( new Label( this ) );
CountLabel.MinimumWidth = 0;
CountLabel.MaximumWidth = 96;
CountLabel.Alignment = TextFlag.RightCenter;
ReloadMaterials();
SetPreviewSize( PreviewSize );
RefreshList();
UpdateSelectedMaterialUi();
}
private void Reload()
{
ReloadMaterials();
RefreshList();
UpdateSelectedMaterialUi();
}
private static void AddSmallLabel( Layout row, string text, int fixedWidth = 0 )
{
var label = row.Add( new Label( text ) );
label.Alignment = fixedWidth > 0 ? TextFlag.RightCenter : TextFlag.LeftCenter;
label.FixedWidth = fixedWidth > 0 ? fixedWidth : text.Length * 6 + 4;
}
private static void StyleBottomInput( Widget widget, int fixedHeight = 22 )
{
widget.FixedHeight = fixedHeight;
widget.SetStyles(
"background-color: #2d3136; " +
"border: 1px solid #555c64; " +
"border-radius: 2px; " +
"color: #f2f2f2; " +
"padding-top: 0px; " +
"padding-bottom: 0px; " +
"padding-left: 5px; " +
"padding-right: 5px;" );
}
private void ReloadMaterials()
{
var menuPath = EditorUtility.Projects.GetAll()
.FirstOrDefault( x => x.Config.Ident == "menu" )
?.GetAssetsPath()
.NormalizeFilename( false );
AllMaterials = AssetSystem.All
.Where( IsBrowsableMaterial )
.Where( x => !IsMenuAsset( x, menuPath ) )
.OrderBy( TextureDisplayName, StringComparer.OrdinalIgnoreCase )
.ToList();
RebuildKeywords();
}
private static bool IsBrowsableMaterial( Asset asset )
{
if ( asset is null )
return false;
if ( asset.AssetType != AssetType.Material )
return false;
if ( asset.AbsolutePath?.Contains( ".sbox/cloud/", StringComparison.OrdinalIgnoreCase ) ?? false )
return false;
return true;
}
private static bool IsMenuAsset( Asset asset, string menuPath )
{
if ( string.IsNullOrEmpty( menuPath ) )
return false;
return asset.AbsolutePath?.NormalizeFilename( false ).StartsWith( menuPath, StringComparison.OrdinalIgnoreCase ) ?? false;
}
private void RebuildKeywords()
{
var tags = AllMaterials
.SelectMany( x => x.Tags )
.Where( x => !string.IsNullOrWhiteSpace( x ) )
.Distinct( StringComparer.OrdinalIgnoreCase )
.OrderBy( x => x, StringComparer.OrdinalIgnoreCase )
.ToList();
KeywordsCombo.Clear();
KeywordsCombo.AddItem( "All Keywords", null, () => SetKeywordFilter( string.Empty ) );
foreach ( var tag in tags )
{
var tagValue = tag;
KeywordsCombo.AddItem( tagValue, null, () => SetKeywordFilter( tagValue ) );
}
KeywordsCombo.CurrentIndex = 0;
}
private void SetKeywordFilter( string tag )
{
KeywordFilter = tag ?? string.Empty;
RefreshList();
}
private void SetPreviewSize( int size )
{
PreviewSize = size;
TextureList.SetPreviewSize( PreviewSize );
RefreshList();
}
private void RefreshList()
{
if ( TextureList is null )
return;
IEnumerable<Asset> materials = AllMaterials;
var query = FilterEdit?.Text ?? string.Empty;
if ( !string.IsNullOrWhiteSpace( query ) )
{
var parts = query.Split( ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries );
materials = materials.Where( x => MatchesQuery( x, parts ) );
}
if ( !string.IsNullOrWhiteSpace( KeywordFilter ) )
{
materials = materials.Where( x => x.Tags.Any( tag => tag.Equals( KeywordFilter, StringComparison.OrdinalIgnoreCase ) ) );
}
if ( OnlyUsedTextures?.Value ?? false )
{
var used = HammerMaterialSelection.GetUsedMaterialKeys();
materials = materials.Where( x => used.Contains( x.RelativePath ) || used.Contains( x.AbsolutePath ) || used.Contains( TextureDisplayName( x ) ) );
}
var entries = materials
.OrderBy( TextureDisplayName, StringComparer.OrdinalIgnoreCase )
.Select( x => new HammerTextureEntry( x ) )
.ToList();
TextureList.SetItems( entries );
if ( CountLabel is not null )
{
CountLabel.Text = $"{entries.Count:n0} textures";
CountLabel.Update();
}
if ( SelectedMaterial is not null && entries.All( x => x.Asset != SelectedMaterial ) )
TextureList.UnselectAll();
}
private static bool MatchesQuery( Asset asset, IEnumerable<string> parts )
{
var name = TextureDisplayName( asset );
var type = asset.AssetType?.FriendlyName ?? string.Empty;
IEnumerable<string> tags = asset.Tags;
foreach ( var part in parts )
{
var search = part;
var negated = false;
if ( search.StartsWith( "-" ) )
{
search = search[1..];
negated = true;
}
var matched =
name.Contains( search, StringComparison.OrdinalIgnoreCase ) ||
type.Contains( search, StringComparison.OrdinalIgnoreCase ) ||
tags.Any( x => x.Contains( search, StringComparison.OrdinalIgnoreCase ) );
if ( !negated && !matched )
return false;
if ( negated && matched )
return false;
}
return true;
}
private void SelectMaterial( Asset material )
{
if ( material?.AssetType != AssetType.Material )
return;
SelectedMaterial = material;
EditorUtility.InspectorObject = material;
HammerMaterialSelection.SetCurrentMaterial( material );
UpdateSelectedMaterialUi();
}
private void UpdateSelectedMaterialUi()
{
var hasMaterial = SelectedMaterial is not null;
SelectedTextureLabel.Text = hasMaterial ? TextureDisplayName( SelectedMaterial ) : "";
SelectedTextureLabel.ToolTip = SelectedMaterial?.RelativePath ?? string.Empty;
TextureSizeLabel.Text = GetTextureSizeText( SelectedMaterial );
TextureSizeLabel.ToolTip = SelectedMaterial?.AbsolutePath ?? string.Empty;
MarkButton.Enabled = hasMaterial;
ReplaceButton.Enabled = hasMaterial;
OpenSourceButton.Enabled = hasMaterial;
SelectedTextureLabel.Update();
TextureSizeLabel.Update();
MarkButton.Update();
ReplaceButton.Update();
OpenSourceButton.Update();
}
private static string GetTextureSizeText( Asset asset )
{
if ( asset is null )
return "";
var texturePath = GetPrimaryMaterialTexturePath( asset );
if ( string.IsNullOrWhiteSpace( texturePath ) )
return "";
try
{
var texture = Texture.Load( texturePath );
if ( texture is null || !texture.IsValid() || texture.Width <= 0 || texture.Height <= 0 )
return "";
return $"{texture.Width}x{texture.Height}";
}
catch
{
return "";
}
}
private static string GetPrimaryMaterialTexturePath( Asset asset )
{
if ( string.IsNullOrWhiteSpace( asset?.AbsolutePath ) || !File.Exists( asset.AbsolutePath ) )
return null;
try
{
var properties = QuotedMaterialProperty.Matches( File.ReadAllText( asset.AbsolutePath ) )
.Select( x => (Key: x.Groups["key"].Value, Value: NormalizeTexturePath( x.Groups["value"].Value )) )
.Where( x => IsTexturePath( x.Value ) )
.ToList();
foreach ( var key in PrimaryTextureKeys )
{
var match = properties.FirstOrDefault( x => x.Key.Equals( key, StringComparison.OrdinalIgnoreCase ) );
if ( !string.IsNullOrWhiteSpace( match.Value ) )
return match.Value;
}
var colorMatch = properties.FirstOrDefault( x =>
(x.Key.Contains( "color", StringComparison.OrdinalIgnoreCase ) ||
x.Key.Contains( "albedo", StringComparison.OrdinalIgnoreCase ) ||
x.Key.Contains( "base", StringComparison.OrdinalIgnoreCase )) &&
!x.Key.Contains( "tint", StringComparison.OrdinalIgnoreCase ) );
if ( !string.IsNullOrWhiteSpace( colorMatch.Value ) )
return colorMatch.Value;
return properties.FirstOrDefault().Value;
}
catch
{
return null;
}
}
private static string NormalizeTexturePath( string path )
{
return path?.Trim().Replace( '\\', '/' );
}
private static bool IsTexturePath( string path )
{
var extension = Path.GetExtension( path );
return extension.Equals( ".vtex", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".tga", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".png", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".jpg", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".jpeg", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".tif", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".tiff", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".psd", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".exr", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".hdr", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".bmp", StringComparison.OrdinalIgnoreCase ) ||
extension.Equals( ".webp", StringComparison.OrdinalIgnoreCase );
}
private void MarkSelectedMaterial()
{
if ( SelectedMaterial is null )
return;
HammerMaterialSelection.SelectFacesUsingMaterial( SelectedMaterial );
}
private void ReplaceSelectedMaterial()
{
if ( SelectedMaterial is null )
return;
HammerMaterialSelection.AssignAssetToSelection( SelectedMaterial );
}
private void OpenSelectedSource()
{
OpenMaterialSource( SelectedMaterial );
}
private void OpenMaterialSource( Asset asset )
{
if ( asset is null )
return;
if ( asset.CanOpenInEditor )
{
asset.OpenInEditor();
return;
}
EditorUtility.OpenFileFolder( asset.AbsolutePath );
}
private void OpenInAssetBrowser( Asset asset )
{
if ( asset is null )
return;
LocalAssetBrowser.OpenTo( asset, true );
}
internal static string TextureDisplayName( Asset asset )
{
var path = asset?.RelativePath ?? asset?.Name ?? "";
path = path.NormalizeFilename( false, false );
if ( path.StartsWith( "materials/", StringComparison.OrdinalIgnoreCase ) )
path = path["materials/".Length..];
var extension = Path.GetExtension( path );
if ( !string.IsNullOrEmpty( extension ) )
path = path[..^extension.Length];
return path;
}
}
internal sealed class HammerTextureList : ListView
{
private int PreviewSize = 128;
public Action<Asset> OnTextureSelected;
public Action<Asset> OnTextureActivated;
public Action<Asset> OnOpenInEditor;
public Action<Asset> OnOpenInAssetBrowser;
public HammerTextureList( Widget parent ) : base( parent )
{
MultiSelect = false;
FocusMode = FocusMode.TabOrClick;
Margin = 2;
ItemSpacing = 2;
ItemPaint = PaintTextureItem;
ItemSelected = item => SelectTexture( item, false );
ItemActivated = item => SelectTexture( item, true );
ItemDrag = StartTextureDrag;
ItemContextMenu = OpenTextureContextMenu;
ItemScrollEnter = item => (item as HammerTextureEntry)?.OnScrollEnter();
ItemScrollExit = item => (item as HammerTextureEntry)?.OnScrollExit();
OnPaintOverride = PaintBackground;
}
public void SetPreviewSize( int previewSize )
{
PreviewSize = previewSize;
ItemSize = new Vector2( PreviewSize + 4, PreviewSize + 18 );
Update();
}
public void SetItems( IEnumerable<HammerTextureEntry> entries )
{
base.SetItems( entries.Cast<object>() );
Update();
}
private bool PaintBackground()
{
Paint.ClearPen();
Paint.SetBrush( Color.Black );
Paint.DrawRect( LocalRect );
return false;
}
private void SelectTexture( object item, bool activated )
{
if ( item is not HammerTextureEntry entry )
return;
if ( activated )
OnTextureActivated?.Invoke( entry.Asset );
else
OnTextureSelected?.Invoke( entry.Asset );
}
private bool StartTextureDrag( object item )
{
if ( item is not HammerTextureEntry entry || entry.Asset is null )
return false;
SelectItem( entry );
SelectTexture( entry, false );
var drag = new Drag( this );
drag.Data.Object = entry.Asset;
drag.Data.Text = entry.Asset.RelativePath;
drag.Data.Url = new Uri( "file:///" + entry.Asset.AbsolutePath );
foreach ( var selected in SelectedItems.OfType<HammerTextureEntry>() )
{
if ( selected == entry || selected.Asset is null )
continue;
drag.Data.Text += "\n" + selected.Asset.RelativePath;
}
drag.Execute();
return true;
}
private void OpenTextureContextMenu( object item )
{
if ( item is not HammerTextureEntry entry )
return;
SelectItem( entry );
SelectTexture( entry, false );
var menu = new ContextMenu( this );
menu.AddOption( "Open in Editor", "edit", () => OnOpenInEditor?.Invoke( entry.Asset ) )
.Enabled = entry.Asset is not null;
menu.AddOption( "Open in Asset Browser", "search", () => OnOpenInAssetBrowser?.Invoke( entry.Asset ) )
.Enabled = entry.Asset is not null;
menu.OpenAtCursor( false );
}
private void PaintTextureItem( VirtualWidget item )
{
if ( item.Object is not HammerTextureEntry entry )
return;
var rect = item.Rect.Shrink( 1 );
var active = Paint.HasPressed;
var selected = Paint.HasSelected || Paint.HasPressed;
var hover = !selected && Paint.HasMouseOver;
Paint.ClearPen();
Paint.SetBrush( Color.Black );
Paint.DrawRect( rect );
if ( selected )
{
Paint.SetPen( Theme.Primary, 2 );
Paint.ClearBrush();
Paint.DrawRect( rect.Grow( -1 ) );
}
else if ( hover )
{
Paint.SetPen( Theme.ControlBackground.Lighten( 0.35f ), 1 );
Paint.ClearBrush();
Paint.DrawRect( rect.Grow( -1 ) );
}
var previewRect = rect;
previewRect.Width = PreviewSize;
previewRect.Height = PreviewSize;
previewRect.Left += MathF.Max( 0, (rect.Width - PreviewSize) * 0.5f );
entry.DrawPreview( previewRect, active );
var nameRect = previewRect;
nameRect.Top = previewRect.Bottom;
nameRect.Height = 12;
Paint.ClearPen();
Paint.SetBrush( selected ? Theme.Primary.Lighten( active ? -0.1f : 0.05f ) : new Color( 0.0f, 0.05f, 0.85f ) );
Paint.DrawRect( nameRect );
Paint.SetDefaultFont( 6 );
Paint.SetPen( Color.White );
var label = Paint.GetElidedText( entry.DisplayName, nameRect.Width - 4, ElideMode.Middle );
Paint.DrawText( nameRect.Shrink( 2, 0 ), label, TextFlag.LeftCenter | TextFlag.SingleLine );
}
}
internal sealed class HammerTextureEntry
{
public Asset Asset { get; }
public string DisplayName { get; }
public HammerTextureEntry( Asset asset )
{
Asset = asset;
DisplayName = HammerTextureBrowserDock.TextureDisplayName( asset );
}
public void OnScrollEnter()
{
if ( Asset is null || Asset.HasCachedThumbnail )
return;
EditorEvent.Register( this );
Asset.GetAssetThumb( true );
}
public void OnScrollExit()
{
if ( Asset is null )
return;
Asset.CancelThumbBuild();
EditorEvent.Unregister( this );
}
public void DrawPreview( Rect rect, bool active )
{
Paint.ClearPen();
Paint.SetBrush( active ? Color.Black.Lighten( 0.08f ) : Color.Black );
Paint.DrawRect( rect );
var thumb = Asset?.GetAssetThumb( true ) ?? AssetType.Material?.Icon128;
if ( thumb is null )
return;
var drawRect = rect;
var scale = MathF.Min( rect.Width / thumb.Width, rect.Height / thumb.Height );
if ( scale > 0 && float.IsFinite( scale ) )
{
drawRect.Width = MathF.Min( rect.Width, thumb.Width * scale );
drawRect.Height = MathF.Min( rect.Height, thumb.Height * scale );
drawRect.Left = rect.Left + (rect.Width - drawRect.Width) * 0.5f;
drawRect.Top = rect.Top + (rect.Height - drawRect.Height) * 0.5f;
}
Paint.BilinearFiltering = true;
Paint.Draw( drawRect, thumb );
Paint.BilinearFiltering = false;
}
}
internal static class HammerMaterialSelection
{
private const BindingFlags StaticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private static Type HammerType;
private static bool HasResolvedHammerType;
public static void SetCurrentMaterial( Asset asset ) => InvokeHammerAssetMethod( "SetCurrentMaterial", asset );
public static void SelectFacesUsingMaterial( Asset asset ) => InvokeHammerAssetMethod( "SelectFacesUsingMaterial", asset );
public static void AssignAssetToSelection( Asset asset ) => InvokeHammerAssetMethod( "AssignAssetToSelection", asset );
public static HashSet<string> GetUsedMaterialKeys()
{
var keys = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
try
{
var activeMap = GetHammerType()?.GetProperty( "ActiveMap", StaticFlags )?.GetValue( null );
if ( activeMap is null )
return keys;
var world = activeMap.GetType().GetProperty( "World", InstanceFlags )?.GetValue( activeMap );
if ( world is null )
return keys;
CollectUsedMaterialKeys( world, keys );
}
catch
{
// Hammer throws when no map is open; in that case "Only used textures" should simply show none.
return keys;
}
return keys;
}
private static void CollectUsedMaterialKeys( object node, HashSet<string> keys )
{
if ( node is null )
return;
var materialMethod = node.GetType().GetMethod( "GetFaceMaterialAssets", InstanceFlags );
if ( materialMethod is not null && materialMethod.Invoke( node, null ) is IEnumerable materials )
{
foreach ( var item in materials )
{
if ( item is not Asset asset )
continue;
keys.Add( asset.RelativePath );
keys.Add( asset.AbsolutePath );
keys.Add( HammerTextureBrowserDock.TextureDisplayName( asset ) );
}
}
var children = node.GetType().GetProperty( "Children", InstanceFlags )?.GetValue( node ) as IEnumerable;
if ( children is null )
return;
foreach ( var child in children )
{
CollectUsedMaterialKeys( child, keys );
}
}
private static void InvokeHammerAssetMethod( string methodName, Asset asset )
{
if ( asset is null )
return;
var method = GetHammerType()?.GetMethod( methodName, StaticFlags, binder: null, types: new[] { typeof( Asset ) }, modifiers: null );
if ( method is null )
return;
try
{
method.Invoke( null, new object[] { asset } );
}
catch
{
// This dock still works as a browser if Hammer is not the active editor surface.
}
}
private static Type GetHammerType()
{
if ( HasResolvedHammerType )
return HammerType;
HasResolvedHammerType = true;
foreach ( var assembly in AppDomain.CurrentDomain.GetAssemblies() )
{
HammerType = assembly.GetType( "Editor.MapEditor.Hammer" );
if ( HammerType is not null )
break;
}
return HammerType;
}
}