Skip to main content
Custom asset editors provide specialized UI for viewing and editing specific asset types. They inherit from FrostyAssetEditor and integrate with the Frosty Editor’s tab system.

FrostyAssetEditor Base Class

From ~/workspace/source/FrostyPlugin/Controls/FrostyAssetEditor.cs:32:
public class FrostyAssetEditor : Control
{
    // Access to the loaded asset
    public object RootObject => asset.RootObject;
    public IEnumerable<object> RootObjects => asset.RootObjects;
    public IEnumerable<object> Objects => asset.Objects;
    public EbxAsset Asset => asset;
    
    // The asset entry being edited
    public AssetEntry AssetEntry { get; private set; }
    
    // Marks the asset as modified
    public bool AssetModified { get; set; }
    
    // Logger for error messages
    protected ILogger logger;
    
    // Loaded asset and dependencies
    protected EbxAsset asset;
    protected Dictionary<Guid, EbxAsset> dependentObjects;
    
    public FrostyAssetEditor(ILogger inLogger)
    {
        logger = inLogger;
    }
}

Creating a Basic Editor

1

Create Editor Class

Inherit from FrostyAssetEditor and pass the logger to the base constructor:
using Frosty.Core.Controls;
using FrostySdk.Interfaces;
using System.Windows.Controls;

namespace MyPlugin
{
    public class MyAssetEditor : FrostyAssetEditor
    {
        public MyAssetEditor(ILogger inLogger) : base(inLogger)
        {
        }
    }
}
2

Define the UI Template

Create a WPF control template in your plugin’s XAML resource dictionary:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:MyPlugin"
                    xmlns:frosty="clr-namespace:Frosty.Core.Controls;assembly=FrostyCore">
    
    <Style TargetType="{x:Type local:MyAssetEditor}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MyAssetEditor}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="300"/>
                        </Grid.ColumnDefinitions>
                        
                        <!-- Main viewer area -->
                        <Border Grid.Column="0" Background="#2D2D30">
                            <!-- Your custom UI here -->
                        </Border>
                        
                        <!-- Splitter -->
                        <GridSplitter Grid.Column="1" Width="5" 
                                      HorizontalAlignment="Stretch"/>
                        
                        <!-- Property grid -->
                        <frosty:FrostyPropertyGrid Grid.Column="2" 
                                                   x:Name="PART_AssetPropertyGrid"
                                                   AssetModified="{Binding AssetModified, 
                                                                   RelativeSource={RelativeSource 
                                                                   TemplatedParent}, 
                                                                   Mode=TwoWay}"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
3

Register the Template

Override the static constructor to register your control template:
static MyAssetEditor()
{
    DefaultStyleKeyProperty.OverrideMetadata(
        typeof(MyAssetEditor), 
        new FrameworkPropertyMetadata(typeof(MyAssetEditor)));
}
4

Load Asset Data

Override LoadAsset to perform custom loading:
protected override EbxAsset LoadAsset(EbxAssetEntry entry)
{
    EbxAsset asset = base.LoadAsset(entry);
    
    // Access the root object
    dynamic rootObj = asset.RootObject;
    
    // Perform custom loading logic
    // e.g., load associated resources
    
    return asset;
}

Accessing Asset Data

Root Object Access

The RootObject property provides access to the main asset object:
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    
    // Access asset data as dynamic
    dynamic rootObj = RootObject;
    
    // Access typed properties
    string assetName = rootObj.Name;
    
    // Load associated resources
    if (rootObj.Resource != null)
    {
        ResAssetEntry resEntry = App.AssetManager.GetResEntry(rootObj.Resource);
        // Load resource data
    }
}

Loading Dependencies

Dependencies are automatically loaded. Access them via the dependentObjects dictionary:
public void LoadDependentAsset(Guid guid)
{
    EbxAsset dependentAsset = GetDependentObject(guid);
    if (dependentAsset != null)
    {
        dynamic depObj = dependentAsset.RootObject;
        // Use dependent asset
    }
}

Adding New Dependencies

From ~/workspace/source/FrostyPlugin/Controls/FrostyAssetEditor.cs:106:
public void AddDependentObject(Guid guid)
{
    if (asset.AddDependency(guid))
    {
        if (!dependentObjects.ContainsKey(guid))
            dependentObjects.Add(guid, App.AssetManager.GetEbx(App.AssetManager.GetEbxEntry(guid)));
    }
}

Modifying Assets

To mark an asset as modified, set the AssetModified property to true. This automatically saves changes: From ~/workspace/source/FrostyPlugin/Controls/FrostyAssetEditor.cs:64:
private static void OnAssetModifiedChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    bool isModified = (bool)e.NewValue;
    if (isModified)
    {
        FrostyAssetEditor editor = o as FrostyAssetEditor;
        
        editor.asset.Update();
        App.AssetManager.ModifyEbx(editor.AssetEntry.Name, editor.asset);
        
        editor.InvokeOnAssetModified();
        editor.AssetModified = false;
    }
}

Example: Modifying Properties

private void OnPropertyChanged()
{
    dynamic rootObj = RootObject;
    
    // Modify a property
    rootObj.SomeProperty = newValue;
    
    // Mark asset as modified
    AssetModified = true;
}

Property Grid Integration

The FrostyPropertyGrid control provides automatic property editing. Name it PART_AssetPropertyGrid in your template:
<frosty:FrostyPropertyGrid x:Name="PART_AssetPropertyGrid"
                           AssetModified="{Binding AssetModified, 
                                           RelativeSource={RelativeSource TemplatedParent}, 
                                           Mode=TwoWay}"/>
The property grid automatically:
  • Displays all properties of the root object
  • Binds to AssetModified to save changes
  • Provides type-appropriate editors (text boxes, dropdowns, etc.)

Custom Toolbar Items

Add custom toolbar buttons to your editor:
public override List<ToolbarItem> RegisterToolbarItems()
{
    List<ToolbarItem> items = base.RegisterToolbarItems();
    
    items.Add(new ToolbarItem(
        "Export",                                    // Text
        "Export asset to file",                      // Tooltip
        "Images/Export.png",                         // Icon path
        new RelayCommand(Export_Click, CanExport)    // Command
    ));
    
    return items;
}

private void Export_Click(object state)
{
    // Handle export
}

private bool CanExport(object state)
{
    return AssetEntry != null;
}

Handling Resources

Loading Texture Resources

private void LoadTexture()
{
    dynamic textureAsset = RootObject;
    
    // Get the resource entry
    ResAssetEntry resEntry = App.AssetManager.GetResEntry(textureAsset.Resource);
    
    // Load as Texture
    Texture texture = App.AssetManager.GetResAs<Texture>(resEntry);
    
    // Access texture properties
    int width = texture.Width;
    int height = texture.Height;
    
    // Get chunk data if needed
    if (texture.ChunkId != Guid.Empty)
    {
        ChunkAssetEntry chunkEntry = App.AssetManager.GetChunkEntry(texture.ChunkId);
        byte[] chunkData = App.AssetManager.GetChunk(chunkEntry);
    }
}

Loading MeshSet Resources

private void LoadMesh()
{
    dynamic meshAsset = RootObject;
    
    // Get mesh resource
    ResAssetEntry resEntry = App.AssetManager.GetResEntry(meshAsset.MeshSetResource);
    MeshSet meshSet = App.AssetManager.GetResAs<MeshSet>(resEntry);
    
    // Iterate LODs
    foreach (MeshSetLod lod in meshSet.Lods)
    {
        // Load chunk data for each LOD
        if (lod.ChunkId != Guid.Empty)
        {
            ChunkAssetEntry chunk = App.AssetManager.GetChunkEntry(lod.ChunkId);
            // Process chunk
        }
    }
}

Complete Example

Here’s a simplified texture editor:
using Frosty.Core.Controls;
using FrostySdk.Interfaces;
using FrostySdk.IO;
using FrostySdk.Managers;
using FrostySdk.Resources;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace TexturePlugin
{
    public class FrostyTextureEditor : FrostyAssetEditor
    {
        private Image imageControl;
        
        static FrostyTextureEditor()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(FrostyTextureEditor),
                new FrameworkPropertyMetadata(typeof(FrostyTextureEditor)));
        }
        
        public FrostyTextureEditor(ILogger inLogger) : base(inLogger)
        {
        }
        
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            
            // Get image control from template
            imageControl = GetTemplateChild("PART_ImageViewer") as Image;
            
            // Load and display texture
            LoadTexture();
        }
        
        private void LoadTexture()
        {
            dynamic textureAsset = RootObject;
            
            // Load resource
            ResAssetEntry resEntry = App.AssetManager.GetResEntry(textureAsset.Resource);
            Texture texture = App.AssetManager.GetResAs<Texture>(resEntry);
            
            // Convert to WPF image source
            BitmapSource bitmap = ConvertTextureToBitmap(texture);
            
            // Display in UI
            if (imageControl != null)
            {
                imageControl.Source = bitmap;
            }
        }
        
        private BitmapSource ConvertTextureToBitmap(Texture texture)
        {
            // Implementation depends on texture format
            // Return converted BitmapSource
            return null;
        }
    }
}

Cleanup

Override Closed() to release resources when the editor is closed:
public override void Closed()
{
    base.Closed();
    
    // Release any resources
    imageControl = null;
    // Dispose any disposable objects
}
Always clean up unmanaged resources (textures, 3D meshes, etc.) to prevent memory leaks.

Next Steps

Custom Actions

Add context menu actions and commands

Attributes Reference

Complete attribute reference

Build docs developers (and LLMs) love