From f6229acf615df090c88f6e3636bb3a2403b6225f Mon Sep 17 00:00:00 2001 From: Gustas Date: Mon, 8 Apr 2024 12:12:47 +0300 Subject: [PATCH] Add WIP asset editor --- OpenRA.Game/FieldSaver.cs | 5 + OpenRA.Game/Graphics/Sprite.cs | 3 + OpenRA.Game/Traits/TraitsInterfaces.cs | 7 + .../Graphics/DefaultSpriteSequence.cs | 25 +- OpenRA.Mods.Common/Traits/Armament.cs | 1 + OpenRA.Mods.Common/Traits/Buildings/Exit.cs | 2 + OpenRA.Mods.Common/Traits/HitShape.cs | 1 + OpenRA.Mods.Common/Traits/Interactable.cs | 2 + .../Traits/Render/WithIdleOverlay.cs | 1 + OpenRA.Mods.Common/Traits/Turreted.cs | 1 + .../Widgets/Logic/AssetEditorLogic.cs | 940 ++++++++++++++++++ .../Widgets/Logic/MainMenuLogic.cs | 15 + OpenRA.Mods.D2k/Graphics/D2kSpriteSequence.cs | 6 +- mods/common/chrome/asseteditor.yaml | 280 ++++++ mods/common/chrome/mainmenu.yaml | 11 +- mods/common/languages/chrome/en.ftl | 11 + mods/common/languages/en.ftl | 6 + mods/d2k/chrome/mainmenu.yaml | 11 +- mods/d2k/mod.yaml | 1 + mods/ra/mod.yaml | 1 + mods/ts/mod.yaml | 1 + 21 files changed, 1315 insertions(+), 16 deletions(-) create mode 100644 OpenRA.Mods.Common/Widgets/Logic/AssetEditorLogic.cs create mode 100644 mods/common/chrome/asseteditor.yaml diff --git a/OpenRA.Game/FieldSaver.cs b/OpenRA.Game/FieldSaver.cs index b75df1415501..097a8a60a3f2 100644 --- a/OpenRA.Game/FieldSaver.cs +++ b/OpenRA.Game/FieldSaver.cs @@ -67,6 +67,11 @@ public static MiniYamlNode SaveField(object o, string field) return new MiniYamlNode(field, FormatValue(o, o.GetType().GetField(field))); } + public static MiniYamlNode SaveField(object o, FieldInfo field) + { + return new MiniYamlNode(field.Name, FormatValue(o, field)); + } + public static string FormatValue(object v) { if (v == null) diff --git a/OpenRA.Game/Graphics/Sprite.cs b/OpenRA.Game/Graphics/Sprite.cs index 2ddc0e29b8e9..64601a3a7818 100644 --- a/OpenRA.Game/Graphics/Sprite.cs +++ b/OpenRA.Game/Graphics/Sprite.cs @@ -11,6 +11,7 @@ using System; using OpenRA.Primitives; +using OpenRA.Traits; namespace OpenRA.Graphics { @@ -22,6 +23,8 @@ public class Sprite public readonly TextureChannel Channel; public readonly float ZRamp; public readonly float3 Size; + + [AssetEditor] public readonly float3 Offset; public readonly float Top, Left, Bottom, Right; diff --git a/OpenRA.Game/Traits/TraitsInterfaces.cs b/OpenRA.Game/Traits/TraitsInterfaces.cs index ffa8fe3c5ceb..4fd915aac43e 100644 --- a/OpenRA.Game/Traits/TraitsInterfaces.cs +++ b/OpenRA.Game/Traits/TraitsInterfaces.cs @@ -27,6 +27,13 @@ namespace OpenRA.Traits [AttributeUsage(AttributeTargets.Interface)] public sealed class RequireExplicitImplementationAttribute : Attribute { } + [AttributeUsage(AttributeTargets.Field)] + public sealed class AssetEditorAttribute : Attribute + { + public readonly string[] EditInsideMembers; + public AssetEditorAttribute(string[] editInsideMembers = null) { EditInsideMembers = editInsideMembers; } + } + [Flags] public enum DamageState { diff --git a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs index 6f99ed794af3..31d2b0e1f6d8 100644 --- a/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs +++ b/OpenRA.Mods.Common/Graphics/DefaultSpriteSequence.cs @@ -16,6 +16,7 @@ using System.Linq; using OpenRA.Graphics; using OpenRA.Primitives; +using OpenRA.Traits; namespace OpenRA.Mods.Common.Graphics { @@ -206,8 +207,9 @@ public ReservationInfo(string filename, List loadFrames, int[] frames, Mini protected readonly ISpriteSequenceLoader Loader; - protected string image; + public readonly string Image; protected List spritesToLoad = new(); + protected Sprite[] sprites; protected Sprite[] shadowSprites; protected bool reverseFacings; @@ -222,6 +224,11 @@ public ReservationInfo(string filename, List loadFrames, int[] frames, Mini protected int facings; protected int? interpolatedFacings; protected int tick; + + [AssetEditor(new string[] { nameof(sprites), nameof(shadowSprites) })] + protected float3 offset; + + [AssetEditor] protected int zOffset; protected int shadowZOffset; protected bool ignoreWorldTint; @@ -236,7 +243,7 @@ public ReservationInfo(string filename, List loadFrames, int[] frames, Mini protected void ThrowIfUnresolved() { if (bounds == null) - throw new InvalidOperationException($"Unable to query unresolved sequence {image}.{Name}."); + throw new InvalidOperationException($"Unable to query unresolved sequence {Image}.{Name}."); } int ISpriteSequence.Length @@ -367,7 +374,7 @@ protected virtual IEnumerable ParseCombineFilenames(ModData mod public DefaultSpriteSequence(SpriteCache cache, ISpriteSequenceLoader loader, string image, string sequence, MiniYaml data, MiniYaml defaults) { - this.image = image; + Image = image; Name = sequence; Loader = loader; @@ -432,7 +439,7 @@ public virtual void ReserveSprites(ModData modData, string tileset, SpriteCache var flipX = LoadField(FlipX, data, defaults); var flipY = LoadField(FlipY, data, defaults); var zRamp = LoadField(ZRamp, data, defaults); - var offset = LoadField(Offset, data, defaults); + offset = LoadField(Offset, data, defaults); var blendMode = LoadField(BlendMode, data, defaults); var combineNode = data.NodeWithKeyOrDefault(Combine.Key); @@ -522,7 +529,7 @@ public virtual void ResolveSprites(SpriteCache cache) if (alpha.Length == 1) alpha = Exts.MakeArray(length.Value, _ => alpha[0]); else if (alpha.Length != length.Value) - throw new YamlException($"Sequence {image}.{Name} must define either 1 or {length.Value} Alpha values."); + throw new YamlException($"Sequence {Image}.{Name} must define either 1 or {length.Value} Alpha values."); } else if (alphaFade) alpha = Exts.MakeArray(length.Value, i => float2.Lerp(1f, 0f, i / (length.Value - 1f))); @@ -536,12 +543,12 @@ public virtual void ResolveSprites(SpriteCache cache) } if (index.Count == 0) - throw new YamlException($"Sequence {image}.{Name} does not define any frames."); + throw new YamlException($"Sequence {Image}.{Name} does not define any frames."); var minIndex = index.Min(); var maxIndex = index.Max(); if (minIndex < 0 || maxIndex >= allSprites.Length) - throw new YamlException($"Sequence {image}.{Name} uses frames between {minIndex}..{maxIndex}, but only 0..{allSprites.Length - 1} exist."); + throw new YamlException($"Sequence {Image}.{Name} uses frames between {minIndex}..{maxIndex}, but only 0..{allSprites.Length - 1} exist."); sprites = index.Select(f => allSprites[f]).ToArray(); if (shadowStart >= 0) @@ -583,7 +590,7 @@ public Sprite GetShadow(int frame, WAngle facing) var index = GetFacingFrameOffset(facing) * length.Value + frame % length.Value; var sprite = shadowSprites[index]; if (sprite == null) - throw new InvalidOperationException($"Attempted to query unloaded shadow sprite from {image}.{Name} frame={frame} facing={facing}."); + throw new InvalidOperationException($"Attempted to query unloaded shadow sprite from {Image}.{Name} frame={frame} facing={facing}."); return sprite; } @@ -594,7 +601,7 @@ public virtual Sprite GetSprite(int frame, WAngle facing) var index = GetFacingFrameOffset(facing) * length.Value + frame % length.Value; var sprite = sprites[index]; if (sprite == null) - throw new InvalidOperationException($"Attempted to query unloaded sprite from {image}.{Name} frame={frame} facing={facing}."); + throw new InvalidOperationException($"Attempted to query unloaded sprite from {Image}.{Name} frame={frame} facing={facing}."); return sprite; } diff --git a/OpenRA.Mods.Common/Traits/Armament.cs b/OpenRA.Mods.Common/Traits/Armament.cs index dbf6929d48c0..51d5cc1b0b02 100644 --- a/OpenRA.Mods.Common/Traits/Armament.cs +++ b/OpenRA.Mods.Common/Traits/Armament.cs @@ -40,6 +40,7 @@ public class ArmamentInfo : PausableConditionalTraitInfo, Requires(); diff --git a/OpenRA.Mods.Common/Traits/Buildings/Exit.cs b/OpenRA.Mods.Common/Traits/Buildings/Exit.cs index c75fd5aa451f..ee8351374edd 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/Exit.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/Exit.cs @@ -19,9 +19,11 @@ namespace OpenRA.Mods.Common.Traits [Desc("Where the unit should leave the building. Multiples are allowed if IDs are added: Exit@2, ...")] public class ExitInfo : ConditionalTraitInfo, Requires { + [AssetEditor] [Desc("Offset at which that the exiting actor is spawned relative to the center of the producing actor.")] public readonly WVec SpawnOffset = WVec.Zero; + [AssetEditor] [Desc("Cell offset where the exiting actor enters the ActorMap relative to the topleft cell of the producing actor.")] public readonly CVec ExitCell = CVec.Zero; public readonly WAngle? Facing = null; diff --git a/OpenRA.Mods.Common/Traits/HitShape.cs b/OpenRA.Mods.Common/Traits/HitShape.cs index 00c5039fcff0..48d35f322f52 100644 --- a/OpenRA.Mods.Common/Traits/HitShape.cs +++ b/OpenRA.Mods.Common/Traits/HitShape.cs @@ -25,6 +25,7 @@ public class HitShapeInfo : ConditionalTraitInfo, Requires [Desc("Name of turret this shape is linked to. Leave empty to link shape to body.")] public readonly string Turret = null; + [AssetEditor] [Desc("Create a targetable position for each offset listed here (relative to CenterPosition).")] public readonly WVec[] TargetableOffsets = { WVec.Zero }; diff --git a/OpenRA.Mods.Common/Traits/Interactable.cs b/OpenRA.Mods.Common/Traits/Interactable.cs index 57a5b4d01297..75232c966c1e 100644 --- a/OpenRA.Mods.Common/Traits/Interactable.cs +++ b/OpenRA.Mods.Common/Traits/Interactable.cs @@ -19,12 +19,14 @@ namespace OpenRA.Mods.Common.Traits [Desc("Used to enable mouse interaction on actors that are not Selectable.")] public class InteractableInfo : TraitInfo, IMouseBoundsInfo { + [AssetEditor] [Desc("Defines a custom rectangle for mouse interaction with the actor.", "If null, the engine will guess an appropriate size based on the With*Body trait.", "The first two numbers define the width and height of the rectangle as a world distance.", "The (optional) second two numbers define an x and y offset from the actor center.")] public readonly WDist[] Bounds = null; + [AssetEditor] [Desc("Defines a custom rectangle for Decorations (e.g. the selection box).", "If null, Bounds will be used instead")] public readonly WDist[] DecorationBounds = null; diff --git a/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs b/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs index 5cc27f3bbf31..f21922cadb70 100644 --- a/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs +++ b/OpenRA.Mods.Common/Traits/Render/WithIdleOverlay.cs @@ -31,6 +31,7 @@ public class WithIdleOverlayInfo : PausableConditionalTraitInfo, IRenderActorPre [Desc("Sequence name to use")] public readonly string Sequence = "idle-overlay"; + [AssetEditor] [Desc("Position relative to body")] public readonly WVec Offset = WVec.Zero; diff --git a/OpenRA.Mods.Common/Traits/Turreted.cs b/OpenRA.Mods.Common/Traits/Turreted.cs index 3aa845909bf9..c834aab011ca 100644 --- a/OpenRA.Mods.Common/Traits/Turreted.cs +++ b/OpenRA.Mods.Common/Traits/Turreted.cs @@ -30,6 +30,7 @@ public class TurretedInfo : PausableConditionalTraitInfo, Requires filteredActors = new(); + readonly PlayerReference selectedOwner; + + readonly Widget initContainer; + readonly Widget checkboxOptionTemplate; + readonly Widget sliderOptionTemplate; + readonly Widget dropdownOptionTemplate; + + readonly Widget editorContainer; + readonly List allEditorFields = new(); + readonly List filteredEditorFields = new(); + readonly Widget optionTemplate; + readonly Widget intOptionTemplate; + readonly Widget wVecOptionTemplate; + readonly Widget float3OptionTemplate; + + readonly ScrollPanelWidget optionsContainer; + + readonly HashSet typableFields = new(); + + readonly ActorPreviewWidget preview; + readonly TextFieldWidget searchTextField; + string searchFilter; + readonly SequenceSet customSequences; + + readonly Dictionary>> actorEdits = new(); + readonly Dictionary>> sequenceEdits = new(); + bool edited; + + IActorPreview[] previewCache; + Panel currentPanel; + + AssetType assetTypesToDisplay = AssetType.Sprites | AssetType.Models | AssetType.Traits; + + [Flags] + enum AssetType + { + Sprites = 1, + Models = 2, + Traits = 4, + } + + readonly struct ActorSelectorActor + { + public readonly ActorInfo Actor; + public readonly Dictionary> Fields; + public readonly string[] SearchTerms; + public readonly string Name; + + public ActorSelectorActor(ActorInfo actor, Dictionary> properties, + string[] searchTerms, string name) + { + Actor = actor; + Fields = properties; + SearchTerms = searchTerms; + Name = name; + } + } + + readonly struct AssetFieldSelector + { + public readonly AssetType Type; + public readonly Widget Widget; + + public AssetFieldSelector(AssetType type, Widget widget) + { + Type = type; + Widget = widget; + } + } + + [ObjectCreator.UseCtor] + public AssetEditorLogic(Widget widget, Action onExit, ModData modData, WorldRenderer worldRenderer) + { + world = worldRenderer.World; + selectedOwner = worldRenderer.World.WorldActor.Owner.PlayerReference; + panel = widget; + + var colorPickerPalettes = world.WorldActor.TraitsImplementing() + .SelectMany(p => p.ColorPickerPaletteNames) + .ToArray(); + + var colorManager = modData.DefaultRules.Actors[SystemActors.World].TraitInfo(); + + var colorDropdown = panel.GetOrNull("COLOR"); + if (colorDropdown != null) + { + var color = Game.Settings.Player.Color; + + // colorDropdown.IsDisabled = () => !colorPickerPalettes.Contains(currentPalette); + colorDropdown.IsDisabled = () => true; + colorDropdown.OnMouseDown = _ => colorManager.ShowColorDropDown(colorDropdown, color, null, worldRenderer, c => color = c); + colorDropdown.IsVisible = () => selectedActor != null; + + panel.Get("COLORBLOCK").GetColor = () => color; + } + + var spriteScaleSlider = panel.GetOrNull("SCALE_SLIDER"); + if (spriteScaleSlider != null) + { + spriteScaleSlider.OnChange += x => preview.SetScale(x); + spriteScaleSlider.GetValue = () => preview.Scale; + } + + var rollSlider = panel.GetOrNull("ROLL_SLIDER"); + if (rollSlider != null) + { + rollSlider.OnChange += x => + { + var roll = (int)x; + modelOrientation = modelOrientation.WithRoll(new WAngle(roll)); + }; + + rollSlider.GetValue = () => modelOrientation.Roll.Angle; + } + + var pitchSlider = panel.GetOrNull("PITCH_SLIDER"); + if (pitchSlider != null) + { + pitchSlider.OnChange += x => + { + var pitch = (int)x; + modelOrientation = modelOrientation.WithPitch(new WAngle(pitch)); + }; + + pitchSlider.GetValue = () => modelOrientation.Pitch.Angle; + } + + var yawSlider = panel.GetOrNull("YAW_SLIDER"); + if (yawSlider != null) + { + yawSlider.OnChange += x => + { + var yaw = (int)x; + modelOrientation = modelOrientation.WithYaw(new WAngle(yaw)); + }; + + yawSlider.GetValue = () => modelOrientation.Yaw.Angle; + } + + var assetTypeDropdown = panel.GetOrNull("TYPES_DROPDOWN"); + if (assetTypeDropdown != null) + { + var assetTypesPanel = CreateAssetTypesPanel(); + assetTypeDropdown.OnMouseDown = _ => + { + assetTypeDropdown.RemovePanel(); + assetTypeDropdown.AttachPanel(assetTypesPanel); + }; + } + + actorList = panel.Get("ASSET_LIST"); + template = panel.Get("ASSET_TEMPLATE"); + + rules = world.Map.Rules; + var allActorsTemp = new List(); + foreach (var a in rules.Actors.Values) + { + // Partial templates are not allowed. + if (a.Name.Contains(ActorInfo.AbstractActorPrefix)) + continue; + + // Actor must have a preview associated with it. + if (!a.HasTraitInfo()) + continue; + + var (actor, properties) = Clone(a); + + var editorData = actor.TraitInfoOrDefault(); + + // Actor must be included in at least one category. + if (editorData == null || editorData.Categories == null) + continue; + + var tooltip = actor.TraitInfos().FirstOrDefault(ti => ti.EnabledByDefault); + var searchTerms = new List() { actor.Name }; + if (tooltip != null) + { + var actorName = TranslationProvider.GetString(tooltip.Name); + searchTerms.Add(actorName); + allActorsTemp.Add(new ActorSelectorActor(actor, properties, searchTerms.ToArray(), $"{actorName} ({actor.Name})")); + } + else + allActorsTemp.Add(new ActorSelectorActor(actor, properties, searchTerms.ToArray(), actor.Name)); + } + + customSequences = Clone(world.Map.Sequences); + preview = panel.Get("ACTOR_PREVIEW"); + preview.IsVisible = () => selectedActor != null; + preview.Sequences = customSequences; + + initContainer = panel.Get("INITS_SCROLLPANEL"); + checkboxOptionTemplate = initContainer.Get("CHECKBOX_OPTION_TEMPLATE"); + sliderOptionTemplate = initContainer.Get("SLIDER_OPTION_TEMPLATE"); + dropdownOptionTemplate = initContainer.Get("DROPDOWN_OPTION_TEMPLATE"); + + editorContainer = panel.Get("EDITOR_SCROLLPANEL"); + optionTemplate = editorContainer.Get("OPTION_TEMPLATE"); + intOptionTemplate = optionTemplate.Get("INT_OPTION_TEMPLATE"); + optionTemplate.RemoveChild(intOptionTemplate); + wVecOptionTemplate = optionTemplate.Get("WVEC_OPTION_TEMPLATE"); + optionTemplate.RemoveChild(wVecOptionTemplate); + float3OptionTemplate = optionTemplate.Get("FLOAT3_OPTION_TEMPLATE"); + optionTemplate.RemoveChild(float3OptionTemplate); + + optionsContainer = panel.Get("OPTIONS_SCROLLPANEL"); + SettingsUtils.AdjustSettingsScrollPanelLayout(optionsContainer); + + allActors = allActorsTemp.ToArray(); + filteredActors = allActors.ToList(); + + InitializeActorList(); + + searchTextField = widget.Get("SEARCH_TEXTFIELD"); + searchTextField.OnEscKey = _ => + { + if (string.IsNullOrEmpty(searchTextField.Text)) + searchTextField.YieldKeyboardFocus(); + else + { + searchTextField.Text = ""; + searchTextField.OnTextEdited(); + } + + return true; + }; + + searchTextField.OnTextEdited = () => + { + searchFilter = searchTextField.Text.Trim(); + filteredActors.Clear(); + + if (!string.IsNullOrEmpty(searchFilter)) + filteredActors.AddRange(allActors.Where(t => t.SearchTerms.Any( + s => s.Contains(searchFilter, StringComparison.CurrentCultureIgnoreCase)))); + else + filteredActors.AddRange(allActors); + + InitializeActorList(); + }; + + var saveButton = panel.GetOrNull("EXPORT_BUTTON"); + if (saveButton != null) + { + saveButton.OnClick = Export; + saveButton.IsDisabled = () => !edited; + } + + var editorButton = panel.GetOrNull("EDITOR_BUTTON"); + if (editorButton != null) + { + editorButton.OnClick = () => currentPanel = Panel.Editor; + editorButton.IsHighlighted = () => currentPanel == Panel.Editor; + editorContainer.IsVisible = () => currentPanel == Panel.Editor; + } + + var optionsButton = panel.GetOrNull("OPTIONS_BUTTON"); + if (optionsButton != null) + { + optionsButton.OnClick = () => currentPanel = Panel.Options; + optionsButton.IsHighlighted = () => currentPanel == Panel.Options; + initContainer.IsVisible = () => currentPanel == Panel.Options; + } + + var initsButton = panel.GetOrNull("INITS_BUTTON"); + if (initsButton != null) + { + initsButton.OnClick = () => currentPanel = Panel.Inits; + initsButton.IsHighlighted = () => currentPanel == Panel.Inits; + optionsContainer.IsVisible = () => currentPanel == Panel.Inits; + } + + var closeButton = panel.GetOrNull("CLOSE_BUTTON"); + if (closeButton != null) + { + closeButton.OnClick = () => + { + if (edited) + { + ConfirmationDialogs.ButtonPrompt(modData, + title: ExitEditorTitle, + text: ExitEditorPrompt, + onConfirm: () => { Ui.CloseWindow(); onExit(); }, + confirmText: ExitEditorConfirm, + onCancel: () => { }); + } + else + { + Ui.CloseWindow(); + onExit(); + } + }; + } + } + + void SetUpTextField(TextFieldWidget textField, string initialValue, Action onTextEdited) + { + textField.Text = initialValue; + textField.OnTextEdited = () => onTextEdited(textField.Text); + + textField.OnEscKey = _ => { textField.YieldKeyboardFocus(); return true; }; + textField.OnEnterKey = _ => { textField.YieldKeyboardFocus(); return true; }; + typableFields.Add(textField); + } + + Widget CreateAssetTypesPanel() + { + var assetTypesPanel = Ui.LoadWidget("ASSET_TYPES_PANEL", null, new WidgetArgs()); + var assetTypeTemplate = assetTypesPanel.Get("ASSET_TYPE_TEMPLATE"); + + foreach (var type in new[] { AssetType.Sprites, AssetType.Models, AssetType.Traits }) + { + var assetType = (CheckboxWidget)assetTypeTemplate.Clone(); + var text = type.ToString(); + assetType.GetText = () => text; + assetType.IsChecked = () => assetTypesToDisplay.HasFlag(type); + assetType.IsVisible = () => true; + assetType.OnClick = () => + { + assetTypesToDisplay ^= type; + UpdateEditorFields(); + }; + + assetTypesPanel.AddChild(assetType); + } + + return assetTypesPanel; + } + + Widget SetEditorFieldsInner(Type fieldType, string fieldName, object initialValue, Action setValue) + { + if (fieldType == typeof(int)) + { + var template = intOptionTemplate.Clone(); + template.Get("LABEL").GetText = () => fieldName; + + SetUpTextField(template.Get("VALUE"), + initialValue.ToString(), + text => + { + if (int.TryParse(text, out var result)) + setValue(result); + }); + + return template; + } + else if (fieldType == typeof(WVec)) + { + var template = wVecOptionTemplate.Clone(); + template.Get("LABEL").GetText = () => fieldName; + var val = (WVec)initialValue; + + SetUpTextField(template.Get("VALUEX"), + val.X.ToString(CultureInfo.InvariantCulture), + text => + { + if (int.TryParse(text, out var result)) + { + val = new WVec(result, val.Y, val.Z); + setValue(val); + } + }); + + SetUpTextField(template.Get("VALUEY"), + val.Y.ToString(CultureInfo.InvariantCulture), + text => + { + if (int.TryParse(text, out var result)) + { + val = new WVec(val.X, result, val.Z); + setValue(val); + } + }); + + SetUpTextField(template.Get("VALUEZ"), + val.Z.ToString(CultureInfo.InvariantCulture), + text => + { + if (int.TryParse(text, out var result)) + { + val = new WVec(val.X, val.Y, result); + setValue(val); + } + }); + + return template; + } + else if (fieldType == typeof(float3)) + { + var template = float3OptionTemplate.Clone(); + template.Get("LABEL").GetText = () => fieldName; + var val = (float3)initialValue; + + SetUpTextField(template.Get("VALUEX"), + val.X.ToString(CultureInfo.InvariantCulture), + text => + { + if (float.TryParse(text, out var result)) + { + val = new float3(result, val.Y, val.Z); + setValue(val); + } + }); + + SetUpTextField(template.Get("VALUEY"), + val.Y.ToString(CultureInfo.InvariantCulture), + text => + { + if (float.TryParse(text, out var result)) + { + val = new float3(val.X, result, val.Z); + setValue(val); + } + }); + + SetUpTextField(template.Get("VALUEZ"), + val.Z.ToString(CultureInfo.InvariantCulture), + text => + { + if (float.TryParse(text, out var result)) + { + val = new float3(val.X, val.Y, result); + setValue(val); + } + }); + + return template; + } + + return null; + } + + Widget SetEditorFields(FieldInfo field, object obj, Action editAction) + { + var attribute = field.GetCustomAttribute(); + if (attribute == null) + return null; + + if (attribute.EditInsideMembers != null) + { + if (field.FieldType != typeof(float3)) + return null; + + List<(object Obj, FieldInfo Field)> fields = new(); + foreach (var member in attribute.EditInsideMembers) + { + if (obj.GetType() + .GetField(member, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(obj) is not IEnumerable collection) + break; + + foreach (var item in collection) + { + if (item != null) + { + foreach (var innerField in item.GetType() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var innerAttribute = innerField.GetCustomAttribute(); + if (innerAttribute != null && innerField.FieldType == typeof(float3)) + fields.Add((item, innerField)); + } + } + } + } + + if (fields.Count > 0) + { + var structVal = (float3)field.GetValue(obj); + return SetEditorFieldsInner(typeof(float3), field.Name, structVal, val => + { + var diff = (float3)val - structVal; + structVal = (float3)val; + foreach (var (o, f) in fields) + { + var fVal = f.GetValue(o); + f.SetValue(o, (float3)fVal + diff); + } + + field.SetValue(obj, val); + editAction(field.Name, val); + }); + } + } + else + return SetEditorFieldsInner(field.FieldType, field.Name, field.GetValue(obj), val => + { + field.SetValue(obj, val); + editAction(field.Name, val); + }); + + return null; + } + + Widget SetEditorTemplate(string name, ICollection widgets) + { + if (widgets.Count > 0) + { + var template = optionTemplate.Clone(); + template.Get("TITLE").GetText = () => name; + + var height = 0; + foreach (var w in widgets) + { + template.AddChild(w); + if (height == 0) + height = w.Bounds.Y + w.Bounds.Height; + else + { + w.Bounds.Y = height; + height += w.Bounds.Height; + template.Bounds.Height += w.Bounds.Height; + } + } + + return template; + } + + return null; + } + + void UpdateEditorFields() + { + filteredEditorFields.Clear(); + foreach (var f in allEditorFields) + if (assetTypesToDisplay.HasFlag(f.Type)) + filteredEditorFields.Add(f); + + editorContainer.RemoveChildren(); + foreach (var f in filteredEditorFields) + editorContainer.AddChild(f.Widget); + } + + void SetupSequenceWidgets() + { + // This can be an expensive opperation, so we only do it when the preview changes. + if (previewCache == preview.Preview) + return; + + previewCache = preview.Preview; + + allEditorFields.RemoveAll(f => f.Type == AssetType.Sprites); + + var usedSequences = new HashSet(); + foreach (var p in previewCache) + { + if (p is SpriteActorPreview sap && sap.Animation.CurrentSequence != null) + { + var sequence = sap.Animation.CurrentSequence; + var image = "undefined-sequence"; + var sequenceName = sequence.Name; + if (sequence is DefaultSpriteSequence dss) + { + image = dss.Image; + sequenceName = $"{dss.Image}.{sequenceName}"; + + if (!usedSequences.Add(sequenceName)) + continue; + } + + var widget = SetEditorTemplate(sequenceName, sequence.GetType() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(f => SetEditorFields(f, sequence, (field, value) => EditSequence(image, sequence.Name, field, value))) + .Where(w => w != null) + .ToList()); + + if (widget != null) + allEditorFields.Add(new AssetFieldSelector(AssetType.Sprites, widget)); + } + } + + UpdateEditorFields(); + } + + void SetPreview(ActorSelectorActor a) + { + allEditorFields.Clear(); + initContainer.RemoveChildren(); + foreach (var f in typableFields) + f.YieldKeyboardFocus(); + + var actor = a.Actor; + + selectedActor = actor; + var td = new TypeDictionary + { + new OwnerInit(selectedOwner.Name), + new FactionInit(selectedOwner.Faction) + }; + foreach (var api in actor.TraitInfos()) + foreach (var o in api.ActorPreviewInits(actor, ActorPreviewType.ColorPicker)) + td.Add(o); + + preview.SetPreview(actor, td); + + foreach (var editableProperties in a.Fields) + { + var trait = editableProperties.Key; + var traitName = string.IsNullOrEmpty(trait.InstanceName) + ? trait.GetType().Name[..^4] + : trait.GetType().Name[..^4] + '@' + trait.InstanceName; + + var widget = SetEditorTemplate(traitName, + editableProperties.Value + .Select(f => SetEditorFields(f, trait, (field, value) => EditActor(actor.Name, traitName, field, value))) + .Where(w => w != null) + .ToList()); + + if (widget != null) + allEditorFields.Add(new AssetFieldSelector(AssetType.Traits, widget)); + } + + // Add new children for inits + var options = actor.TraitInfos() + .SelectMany(t => t.ActorOptions(actor, world)) + .OrderBy(o => o.DisplayOrder); + + foreach (var o in options) + { + if (o.DisplayMapEditorOnly) + continue; + + if (o is EditorActorCheckbox co) + { + var checkboxContainer = checkboxOptionTemplate.Clone(); + var checkbox = checkboxContainer.Get("OPTION"); + checkbox.GetText = () => co.Name; + + checkbox.IsChecked = () => co.GetValue(preview); + checkbox.OnClick = () => + { + co.OnChange(preview, co.GetValue(preview) ^ true); + SetupSequenceWidgets(); + }; + + initContainer.AddChild(checkboxContainer); + } + else if (o is EditorActorSlider so) + { + var sliderContainer = sliderOptionTemplate.Clone(); + sliderContainer.Get("LABEL").GetText = () => so.Name; + + var slider = sliderContainer.Get("OPTION"); + slider.MinimumValue = so.MinValue; + slider.MaximumValue = so.MaxValue; + slider.Ticks = so.Ticks; + + so.OnChange(preview, so.GetValue(preview)); + slider.GetValue = () => so.GetValue(preview); + slider.OnChange += value => + { + so.OnChange(preview, value); + SetupSequenceWidgets(); + }; + + var valueField = sliderContainer.GetOrNull("VALUE"); + if (valueField != null) + { + void UpdateValueField(float f) => valueField.Text = ((int)f).ToString(NumberFormatInfo.CurrentInfo); + UpdateValueField(so.GetValue(preview)); + slider.OnChange += UpdateValueField; + + valueField.OnTextEdited = () => + { + if (float.TryParse(valueField.Text, out var result)) + slider.UpdateValue(result); + }; + + valueField.OnEscKey = _ => { valueField.YieldKeyboardFocus(); return true; }; + valueField.OnEnterKey = _ => { valueField.YieldKeyboardFocus(); return true; }; + typableFields.Add(valueField); + } + + initContainer.AddChild(sliderContainer); + } + else if (o is EditorActorDropdown ddo) + { + var dropdownContainer = dropdownOptionTemplate.Clone(); + dropdownContainer.Get("LABEL").GetText = () => ddo.Name; + + var dropdown = dropdownContainer.Get("OPTION"); + ScrollItemWidget DropdownSetup(KeyValuePair option, ScrollItemWidget template) + { + var item = ScrollItemWidget.Setup(template, + () => ddo.GetValue(preview) == option.Key, + () => + { + ddo.OnChange(preview, option.Key); + SetupSequenceWidgets(); + }); + + item.Get("LABEL").GetText = () => option.Value; + return item; + } + + dropdown.GetText = () => ddo.Labels[ddo.GetValue(preview)]; + dropdown.OnClick = () => dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 270, ddo.Labels, DropdownSetup); + + initContainer.AddChild(dropdownContainer); + } + } + + SetupSequenceWidgets(); + } + + public void Export() + { + var actorNodes = new List(); + var sequenceNodes = new List(); + foreach (var actor in actorEdits) + actorNodes.Add(new MiniYamlNode(actor.Key, null, + actor.Value.Select(t => new MiniYamlNode(t.Key, + null, t.Value.Select(f => new MiniYamlNode(f.Key, FieldSaver.FormatValue(f.Value))).ToList())).ToList())); + + foreach (var actor in sequenceEdits) + sequenceNodes.Add(new MiniYamlNode(actor.Key, null, + actor.Value.Select(t => new MiniYamlNode(t.Key, + null, t.Value.Select(f => new MiniYamlNode(f.Key, FieldSaver.FormatValue(f.Value))).ToList())).ToList())); + + if (actorNodes.Count != 0) + actorNodes.WriteToFile(Path.Combine(Platform.SupportDir, "AssetEditorActors.yaml")); + + if (sequenceNodes.Count != 0) + sequenceNodes.WriteToFile(Path.Combine(Platform.SupportDir, "AssetEditorSequences.yaml")); + + edited = false; + } + + void EditActor(string name, string parentName, string fieldName, object value) + { + edited = true; + edited = true; + if (actorEdits.TryGetValue(name, out var actor)) + { + if (actor.TryGetValue(parentName, out var fields)) + fields[fieldName] = value; + else + actor.Add(parentName, new Dictionary() { { fieldName, value } }); + } + else + actorEdits.Add(name, new Dictionary> + { + { + parentName, + new Dictionary() { { fieldName, value } } + } + }); + } + + void EditSequence(string name, string parentName, string fieldName, object value) + { + edited = true; + if (sequenceEdits.TryGetValue(name, out var actor)) + { + if (actor.TryGetValue(parentName, out var fields)) + fields[fieldName] = value; + else + actor.Add(parentName, new Dictionary() { { fieldName, value } }); + } + else + sequenceEdits.Add(name, new Dictionary> + { + { + parentName, + new Dictionary() { { fieldName, value } } + } + }); + } + + public static (ActorInfo Actor, Dictionary> Traits) Clone(ActorInfo actor) + { + var clonedTraits = new List(); + foreach (var trait in actor.TraitInfos()) + clonedTraits.Add((TraitInfo)trait.GetType() + .GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(trait, null)); + + return (new ActorInfo(actor.Name, clonedTraits.ToArray()), GetEditableFields(clonedTraits)); + } + + public static SequenceSet Clone(SequenceSet sequenceSet) + { + var fieldInfo = sequenceSet.GetType().GetField("images", BindingFlags.NonPublic | BindingFlags.Instance); + var images = (IReadOnlyDictionary>)fieldInfo.GetValue(sequenceSet); + var clonedImages = new Dictionary>(); + foreach (var outerEntry in images) + { + var outerKey = outerEntry.Key; + var innerDictionary = outerEntry.Value; + + var clonedInnerDictionary = new Dictionary(); + foreach (var innerEntry in innerDictionary) + { + var spriteSequence = innerEntry.Value; + var type = spriteSequence.GetType(); + var clonedSpriteSequence = (ISpriteSequence)type + .GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(spriteSequence, null); + + // Deep clone necesary fields. + foreach (var field in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)) + { + var attribute = field.GetCustomAttribute(); + if (attribute == null || attribute.EditInsideMembers == null) + continue; + + var value = field.GetValue(spriteSequence); + if (value == null) + continue; + + if (value is Array arr) + { + var arrType = arr.GetType().GetElementType(); + var clonedArray = Array.CreateInstance(arrType, arr.Length); + for (var i = 0; i < arr.Length; i++) + clonedArray.SetValue(arrType.GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(arr.GetValue(i), null), i); + + value = clonedArray; + } + + field.SetValue(clonedSpriteSequence, value); + } + + clonedInnerDictionary.Add(innerEntry.Key, clonedSpriteSequence); + } + + clonedImages.Add(outerKey, clonedInnerDictionary); + } + + var clonedSequenceSet = (SequenceSet)sequenceSet.GetType() + .GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(sequenceSet, null); + + clonedSequenceSet.GetType() + .GetField("images", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(clonedSequenceSet, clonedImages); + + return clonedSequenceSet; + } + + public static Dictionary> GetEditableFields(IEnumerable traitInfos) + { + var editableProperties = new Dictionary>(); + foreach (var traitInfo in traitInfos) + { + var fields = traitInfo.GetType() + .GetFields(BindingFlags.Public | BindingFlags.Instance); + + foreach (var field in fields) + { + if (field.GetCustomAttributes(typeof(AssetEditorAttribute), true).Length > 0) + { + if (editableProperties.TryGetValue(traitInfo, out var list)) + list.Add(field); + else + editableProperties.Add(traitInfo, new List { field }); + } + } + } + + editableProperties.TrimExcess(); + return editableProperties; + } + + void InitializeActorList() + { + actorList.RemoveChildren(); + + foreach (var a in filteredActors) + { + var actor = a.Actor; + var item = ScrollItemWidget.Setup(template, + () => actor == selectedActor, + () => SetPreview(a)); + + var label = item.Get("TITLE"); + WidgetUtils.TruncateLabelToTooltip(label, a.Name); + item.IsVisible = () => true; + actorList.AddChild(item); + } + + if (filteredActors.Count > 0 && (selectedActor == null || !filteredActors.Any(a => a.Actor == selectedActor))) + SetPreview(filteredActors[0]); + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs index 588b6e58bc11..6d55f3ce4071 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs @@ -152,6 +152,7 @@ public MainMenuLogic(Widget widget, World world, ModData modData) var assetBrowserButton = extrasMenu.GetOrNull("ASSETBROWSER_BUTTON"); if (assetBrowserButton != null) + { assetBrowserButton.OnClick = () => { SwitchMenu(MenuType.None); @@ -160,6 +161,20 @@ public MainMenuLogic(Widget widget, World world, ModData modData) { "onExit", () => SwitchMenu(MenuType.Extras) }, }); }; + } + + var assetEditorButton = extrasMenu.GetOrNull("ASSETEDITOR_BUTTON"); + if (assetEditorButton != null) + { + assetEditorButton.OnClick = () => + { + SwitchMenu(MenuType.None); + Game.OpenWindow("ASSETEDITOR_PANEL", new WidgetArgs + { + { "onExit", () => SwitchMenu(MenuType.Extras) }, + }); + }; + } extrasMenu.Get("CREDITS_BUTTON").OnClick = () => { diff --git a/OpenRA.Mods.D2k/Graphics/D2kSpriteSequence.cs b/OpenRA.Mods.D2k/Graphics/D2kSpriteSequence.cs index caf4b5517e67..a2295de7379c 100644 --- a/OpenRA.Mods.D2k/Graphics/D2kSpriteSequence.cs +++ b/OpenRA.Mods.D2k/Graphics/D2kSpriteSequence.cs @@ -161,7 +161,7 @@ public override void ResolveSprites(SpriteCache cache) if (alpha.Length == 1) alpha = Exts.MakeArray(length.Value, _ => alpha[0]); else if (alpha.Length != length.Value) - throw new YamlException($"Sequence {image}.{Name} must define either 1 or {length.Value} Alpha values."); + throw new YamlException($"Sequence {Image}.{Name} must define either 1 or {length.Value} Alpha values."); } else if (alphaFade) alpha = Exts.MakeArray(length.Value, i => float2.Lerp(1f, 0f, i / (length.Value - 1f))); @@ -175,12 +175,12 @@ public override void ResolveSprites(SpriteCache cache) } if (index.Count == 0) - throw new YamlException($"Sequence {image}.{Name} does not define any frames."); + throw new YamlException($"Sequence {Image}.{Name} does not define any frames."); var minIndex = index.Min(); var maxIndex = index.Max(); if (minIndex < 0 || maxIndex >= allSprites.Length) - throw new YamlException($"Sequence {image}.{Name} uses frames between {minIndex}..{maxIndex}, but only 0..{allSprites.Length - 1} exist."); + throw new YamlException($"Sequence {Image}.{Name} uses frames between {minIndex}..{maxIndex}, but only 0..{allSprites.Length - 1} exist."); sprites = index.Select(f => allSprites[f]).ToArray(); if (shadowStart >= 0) diff --git a/mods/common/chrome/asseteditor.yaml b/mods/common/chrome/asseteditor.yaml new file mode 100644 index 000000000000..4c2fcdcc7b4b --- /dev/null +++ b/mods/common/chrome/asseteditor.yaml @@ -0,0 +1,280 @@ +Background@ASSETEDITOR_PANEL: + Logic: AssetEditorLogic + X: (WINDOW_RIGHT - WIDTH) / 2 + Y: (WINDOW_BOTTOM - HEIGHT) / 2 + Width: 900 + Height: 600 + Children: + Label@ASSETBROWSER_TITLE: + Y: 16 + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + Text: label-asseteditor-title + Label@FILENAME_DESC: + X: 20 + Y: 115 + Width: 175 + Height: 25 + Font: TinyBold + Align: Center + Text: label-asseteditor-actorname-filter + TextField@SEARCH_TEXTFIELD: + X: 20 + Y: 140 + Width: 175 + Height: 25 + Type: General + ScrollPanel@ASSET_LIST: + X: 20 + Y: 170 + Width: 175 + Height: PARENT_BOTTOM - 250 + TopBottomSpacing: 6 + ItemSpacing: 4 + Children: + ScrollItem@ASSET_TEMPLATE: + Width: PARENT_RIGHT - 27 + Height: 25 + X: 2 + Visible: false + EnableChildMouseOver: True + Children: + LabelWithTooltip@TITLE: + X: 10 + Width: PARENT_RIGHT - 20 + Height: 25 + TooltipContainer: TOOLTIP_CONTAINER + TooltipTemplate: SIMPLE_TOOLTIP + Background@SPRITE_BG: + X: 195 + Y: 65 + Width: 391 + Height: 455 + Background: dialog3 + Children: + ActorPreview@ACTOR_PREVIEW: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Visible: false + Center: true + RecalculateBounds: false + Button@EDITOR_BUTTON: + X: 586 + Y: 40 + Width: 98 + Height: 25 + Font: Bold + Text: button-editor + Button@OPTIONS_BUTTON: + X: 684 + Y: 40 + Width: 98 + Height: 25 + Font: Bold + Text: button-inits + Button@INITS_BUTTON: + X: 782 + Y: 40 + Width: 98 + Height: 25 + Font: Bold + Text: button-options + Container@EDITOR_BACKGROUND: + X: 586 + Y: 65 + Width: 294 + Height: 455 + Children: + DropDownButton@TYPES_DROPDOWN: + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Text: dropdownbutton-asset-type-dropdown + ScrollPanel@EDITOR_SCROLLPANEL: + Y: 25 + Width: PARENT_RIGHT + Height: PARENT_BOTTOM - 25 + CollapseHiddenChildren: True + TopBottomSpacing: 10 + Children: + Container@OPTION_TEMPLATE: + Width: PARENT_RIGHT - 24 + Height: 42 + Children: + Label@TITLE: + X: 5 + Y: 1 + Width: PARENT_RIGHT + Height: 10 + Font: TinyBold + Align: Center + Container@INT_OPTION_TEMPLATE: + Y: 15 + Width: PARENT_RIGHT + Height: 26 + Children: + Label@LABEL: + X: 5 + Width: 90 + Height: 16 + Align: Right + TextField@VALUE: + X: 100 + Width: 50 + Height: 20 + Type: Integer + Container@WVEC_OPTION_TEMPLATE: + Y: 15 + Width: PARENT_RIGHT + Height: 26 + Children: + Label@LABEL: + X: 5 + Width: 90 + Height: 16 + Align: Right + TextField@VALUEX: + X: 100 + Width: 50 + Height: 20 + Type: Integer + TextField@VALUEY: + X: 160 + Width: 50 + Height: 20 + Type: Integer + TextField@VALUEZ: + X: 210 + Width: 50 + Height: 20 + Type: Integer + Container@FLOAT3_OPTION_TEMPLATE: + Y: 15 + Width: PARENT_RIGHT + Height: 26 + Children: + Label@LABEL: + X: 5 + Width: 90 + Height: 16 + Align: Right + TextField@VALUEX: + X: 100 + Width: 50 + Height: 20 + Type: Float + TextField@VALUEY: + X: 160 + Width: 50 + Height: 20 + Type: Float + TextField@VALUEZ: + X: 210 + Width: 50 + Height: 20 + Type: Float + ScrollPanel@INITS_SCROLLPANEL: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + CollapseHiddenChildren: True + TopBottomSpacing: 10 + ItemSpacing: 15 + Children: + Container@CHECKBOX_OPTION_TEMPLATE: + Width: PARENT_RIGHT - 24 + Height: 22 + Children: + Checkbox@OPTION: + X: 5 + Y: 1 + Width: PARENT_RIGHT - 100 + Height: 20 + Container@SLIDER_OPTION_TEMPLATE: + Width: PARENT_RIGHT - 24 + Height: 22 + Children: + Label@LABEL: + X: 5 + Y: 1 + Width: 55 + Height: 16 + Align: Right + Slider@OPTION: + X: 75 + Y: 1 + Width: 120 + Height: 20 + TextField@VALUE: + X: 210 + Y: 1 + Width: 50 + Height: 20 + Type: Integer + Container@DROPDOWN_OPTION_TEMPLATE: + Width: PARENT_RIGHT - 24 + Height: 27 + Children: + Label@LABEL: + X: 5 + Y: 2 + Width: 55 + Height: 24 + Align: Right + DropDownButton@OPTION: + X: 84 + Y: 1 + Width: PARENT_RIGHT - 84 - 24 + Height: 25 + Font: Bold + ScrollPanel@OPTIONS_SCROLLPANEL: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + CollapseHiddenChildren: True + TopBottomSpacing: 10 + ItemSpacing: 10 + Children: + Container@SCALE_CONTAINER: + Width: PARENT_RIGHT - 24 + Height: 25 + Children: + Label@SCALE: + X: 5 + Width: 50 + Height: 25 + Font: Bold + Align: Right + Text: label-asseteditor-scale + Slider@SCALE_SLIDER: + X: 55 + Width: 200 + Height: 20 + MinimumValue: 0.3 + MaximumValue: 15 + DropDownButton@COLOR: + Width: PARENT_RIGHT - 24 + Width: 80 + Height: 25 + Children: + ColorBlock@COLORBLOCK: + X: 5 + Y: 6 + Width: PARENT_RIGHT - 35 + Height: PARENT_BOTTOM - 12 + Button@EXPORT_BUTTON: + Key: EditorQuickSave + X: PARENT_RIGHT - 345 + Y: PARENT_BOTTOM - 45 + Width: 160 + Height: 25 + Text: button-asseteditor-export + Button@CLOSE_BUTTON: + Key: escape + X: PARENT_RIGHT - 180 + Y: PARENT_BOTTOM - 45 + Width: 160 + Height: 25 + Font: Bold + Text: button-back + TooltipContainer@TOOLTIP_CONTAINER: diff --git a/mods/common/chrome/mainmenu.yaml b/mods/common/chrome/mainmenu.yaml index 88971366db40..c83049c7a419 100644 --- a/mods/common/chrome/mainmenu.yaml +++ b/mods/common/chrome/mainmenu.yaml @@ -169,17 +169,24 @@ Container@MAINMENU: Height: 30 Text: button-extras-menu-assetbrowser Font: Bold - Button@CREDITS_BUTTON: + Button@ASSETEDITOR_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 220 Width: 140 Height: 30 + Text: button-extras-menu-asseteditor + Font: Bold + Button@CREDITS_BUTTON: + X: PARENT_RIGHT / 2 - WIDTH / 2 + Y: 260 + Width: 140 + Height: 30 Text: label-credits-title Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Key: escape - Y: 260 + Y: 300 Width: 140 Height: 30 Text: button-back diff --git a/mods/common/languages/chrome/en.ftl b/mods/common/languages/chrome/en.ftl index 49820431434d..3c237c9d8b1e 100644 --- a/mods/common/languages/chrome/en.ftl +++ b/mods/common/languages/chrome/en.ftl @@ -8,6 +8,16 @@ label-assetbrowser-sprite-scale = Scale: label-assetbrowser-palette-desc = Palette: label-assetbrowser-sprite-bg-error = Error displaying file. See assetbrowser.log for details. +## asseteditor.yaml +label-asseteditor-title = Asset Editor +label-asseteditor-actorname-filter = Filter by name +label-asseteditor-scale = Scale: +button-asseteditor-export = Export +dropdownbutton-asset-type-dropdown = Asset types +button-editor = Edit +button-inits = Inits +button-options = Options + ## color-picker.yaml button-color-chooser-random = Random button-color-chooser-store = Store @@ -307,6 +317,7 @@ button-singleplayer-menu-load = Load button-extras-menu-replays = Replays label-map-editor-title = Map Editor button-extras-menu-assetbrowser = Asset Browser +button-extras-menu-asseteditor = Asset Editor button-map-editor-new-map = New Map button-map-editor-load-map = Load Map dropdownbutton-news-bg-button = Battlefield News diff --git a/mods/common/languages/en.ftl b/mods/common/languages/en.ftl index 37ebb34816ea..be5af8d88ea5 100644 --- a/mods/common/languages/en.ftl +++ b/mods/common/languages/en.ftl @@ -447,6 +447,12 @@ dialog-settings-reset = label-all-packages = All Packages label-length-in-seconds = { $length } sec +## asseteditor.yaml +dialog-asseteditor-exit-editor = + .title = Exit Asset Editor + .prompt = Exit and lose all unsaved changes? + .confirm = Exit + ## ConnectionLogic label-connecting-to-endpoint = Connecting to { $endpoint }... label-could-not-connect-to-target = Could not connect to { $target } diff --git a/mods/d2k/chrome/mainmenu.yaml b/mods/d2k/chrome/mainmenu.yaml index 42c1324bbab6..2d4371cd6b01 100644 --- a/mods/d2k/chrome/mainmenu.yaml +++ b/mods/d2k/chrome/mainmenu.yaml @@ -163,17 +163,24 @@ Container@MAINMENU: Height: 30 Text: button-extras-menu-assetbrowser Font: Bold - Button@CREDITS_BUTTON: + Button@ASSETEDITOR_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Y: 220 Width: 140 Height: 30 + Text: button-extras-menu-asseteditor + Font: Bold + Button@CREDITS_BUTTON: + X: PARENT_RIGHT / 2 - WIDTH / 2 + Y: 260 + Width: 140 + Height: 30 Text: label-credits-title Font: Bold Button@BACK_BUTTON: X: PARENT_RIGHT / 2 - WIDTH / 2 Key: escape - Y: 260 + Y: 300 Width: 140 Height: 30 Text: button-back diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index e11301b8fdc4..eebc8e1cf10f 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -114,6 +114,7 @@ ChromeLayout: common|chrome/musicplayer.yaml d2k|chrome/tooltips.yaml common|chrome/assetbrowser.yaml + common|chrome/asseteditor.yaml d2k|chrome/missionbrowser.yaml common|chrome/confirmation-dialogs.yaml common|chrome/editor.yaml diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 7fd1fb3a9a96..1262d69b0b96 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -133,6 +133,7 @@ ChromeLayout: common|chrome/musicplayer.yaml common|chrome/tooltips.yaml common|chrome/assetbrowser.yaml + common|chrome/asseteditor.yaml common|chrome/missionbrowser.yaml common|chrome/confirmation-dialogs.yaml common|chrome/editor.yaml diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index b06d986f60f1..70b46bb139a7 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -179,6 +179,7 @@ ChromeLayout: common|chrome/musicplayer.yaml common|chrome/tooltips.yaml ts|chrome/assetbrowser.yaml + common|chrome/asseteditor.yaml common|chrome/missionbrowser.yaml common|chrome/confirmation-dialogs.yaml common|chrome/editor.yaml