diff --git a/AnimationEditor/how to/01-create-applet.md b/AnimationEditor/how to/01-create-applet.md new file mode 100644 index 000000000..08a60b9f2 --- /dev/null +++ b/AnimationEditor/how to/01-create-applet.md @@ -0,0 +1,91 @@ +# 01 — Create a new Animation applet (editor) + +This guide shows how to add a new Animation applet (editor) following the same structure as +`AnimationKeyframeEditorViewModel` and `MountAnimationCreatorViewModel`. + +Where to put code +- Place your editor-specific files under `Editors/AnimationEditor//` (this is consistent with the existing editors). +- Example existing editors: + - `Editors/AnimationEditor/AnimationKeyframeEditor/AnimationKeyframeEditorViewModel.cs` + - `Editors/AnimationEditor/MountAnimationCreator/MountAnimationCreatorViewModel.cs` + +High level steps +1. Create a ViewModel class (recommended: derive from `EditorHostBase`). +2. (Optional) Create a custom view `UserControl` if you want a custom UI; otherwise the shared `EditorHostView` is used. +3. Add a `Create(...)` method and wire your SceneObjects and services exactly like the existing editors. +4. Add any helper classes (ViewModels, sub-ViewModels) under your folder. + +Minimal ViewModel skeleton (copy & adapt) + +```csharp +// file: Editors/AnimationEditor/MyApplet/MyAppletViewModel.cs +using System; +using Editors.Shared.Core.Common.BaseControl; +using Editors.Shared.Core.Common; +using Editors.Shared.Core.Common.AnimationPlayer; +using Shared.Core.ToolCreation; +using Microsoft.Extensions.DependencyInjection; // (for DI registration example later) + +namespace Editors.AnimationVisualEditors.MyApplet +{ + public partial class MyAppletViewModel : EditorHostBase + { + public override Type EditorViewModelType => typeof(AnimationEditor.Common.BaseControl.EditorHostView); + + // Dependencies you typically need — match constructor to what DI can provide. + public MyAppletViewModel( + IEditorHostParameters editorHostParameters, + SceneObjectViewModelBuilder sceneObjectViewModelBuilder, + AnimationPlayerViewModel animationPlayerViewModel, + SceneObjectEditor sceneObjectEditor, + IPackFileService pfs, + ISkeletonAnimationLookUpHelper skeletonAnimationLookUpHelper, + SelectionManager selectionManager, + IFileSaveService fileSaveService, + IUiCommandFactory uiCommandFactory) : base(editorHostParameters) + { + DisplayName = "My Applet"; + + // Use the provided builders/services to create default scene objects the same way + // other editors do to ensure things like MountLinkController are initialized. + var riderItem = sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Rider", Microsoft.Xna.Framework.Color.Black, null); + var mountItem = sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Mount", Microsoft.Xna.Framework.Color.Black, null); + mountItem.Data.IsSelectable = true; + + var newAnimAsset = sceneObjectEditor.CreateAsset("IDK", "New Anim", Microsoft.Xna.Framework.Color.Red); + animationPlayerViewModel.RegisterAsset(newAnimAsset); + + Create(riderItem.Data, mountItem.Data, newAnimAsset); + SceneObjects.Add(riderItem); + SceneObjects.Add(mountItem); + } + + internal void Create(SceneObject rider, SceneObject mount, SceneObject newAnimation) + { + // store refs, hook events, create sub-viewmodels etc. + } + } +} +``` + +Notes about using `EditorHostBase` +- The shared `EditorHostView` is used by many editors and is a good default unless you need custom UI. +- Put logic that wires SceneObjects and registers with the `AnimationPlayerViewModel` into the constructor, or into a helper `Create(...)` method similar to existing editors. + +Common pitfalls +- If you leave essential components (like the MountLink controller in the Mount editor) uncreated until some manual initialization, code that expects them on startup can get NullReferenceExceptions. The pattern in the repo is to create at least default Rider/Mount/NewAnim scene objects in the constructor so the editor is ready. +- Be mindful of the non-nullable reference warnings. You can: + - initialize fields eagerly, + - mark them nullable if they legitimately start null, or + - ensure constructors always initialize them. + +Testing +- Build the solution. If there are DI or registration errors the build/run will tell you which types are missing. +- Next step is to register the applet in DI and the editor database (see `02-register-and-debug.md`). + +References +- Example ViewModels to copy from: + - `Editors/AnimationEditor/AnimationKeyframeEditor/AnimationKeyframeEditorViewModel.cs` + - `Editors/AnimationEditor/MountAnimationCreator/MountAnimationCreatorViewModel.cs` + +-- diff --git a/AnimationEditor/how to/02-register-and-debug.md b/AnimationEditor/how to/02-register-and-debug.md new file mode 100644 index 000000000..8e45cf232 --- /dev/null +++ b/AnimationEditor/how to/02-register-and-debug.md @@ -0,0 +1,128 @@ +# 02 — Register your applet in DI and add a dev-config for quick debug + +After you add your ViewModel (see `01-create-applet.md`), register it so the editor host can create and show it. + +1) Add the ViewModel to the animation editors DI container + +Edit the container at: + +``` +TheAssetEditor/Editors/AnimationEditor/DependencyInjectionContainer.cs +``` + +Inside `Register(IServiceCollection serviceCollection)` add a scoped registration for your view model: + +```csharp +serviceCollection.AddScoped(); +``` + +This mirrors how the repo registers existing editors: + +```csharp +serviceCollection.AddScoped(); +serviceCollection.AddScoped(); +``` + +2) Register the editor in the editor database (so it appears in the toolbar/menu) + +In the same `DependencyInjectionContainer`, implement `RegisterTools(IEditorDatabase database)` and add an EditorInfo entry. Example: + +```csharp +EditorInfoBuilder + .Create(EditorEnums.MyApplet_Editor) + .AddToToolbar("My Applet", true) + .Build(database); +``` + +Notes: +- `EditorHostView` is the default shared host view. Use a custom UserControl type here if you created a custom UI view for your applet. +- `EditorEnums.MyApplet_Editor` must exist in the `EditorEnums` enum (see next step). + +3) Add an enum value for your editor + +Open: + +``` +Shared/SharedCore/Shared.Core/ToolCreation/EditorEnums.cs +``` + +Add an entry (for example): + +```csharp +public enum EditorEnums +{ + ... + MyApplet_Editor, + None, +} +``` + +Make sure the enum value is unique and update any usages if necessary. + +4) (Optional) Add a dev-config so your app opens pre-configured for development + +To open your editor automatically (with sample inputs), add a dev-config like the repo's `MountTool` example. Create a class under `Editors/AnimationEditor/MyApplet/DevConfig/MyAppletDevConfig.cs` implementing `IDeveloperConfiguration`. + +Sample DevConfig (copy & adapt from `MountTool`): + +```csharp +using Shared.Core.DevConfig; +using Shared.Core.ToolCreation; +using Shared.Core.PackFiles; // for packfile lookups + +namespace Editors.AnimationVisualEditors.MyApplet.DevConfig +{ + internal class MyAppletDevConfig : IDeveloperConfiguration + { + private readonly IEditorManager _editorManager; + private readonly IPackFileService _packFileService; + + public MyAppletDevConfig(IEditorManager editorManager, IPackFileService packFileService) + { + _editorManager = editorManager; + _packFileService = packFileService; + } + + public void OverrideSettings(ApplicationSettings currentSettings) + { + // if you need to change game settings for development/testing + } + + public void OpenFileOnLoad() + { + // create a debug AnimationToolInput (like MountTool does) + var input = new AnimationToolInput() + { + Mesh = _packFileService.FindFile("variantmeshes\\...\\some.variantmeshdefinition"), + Animation = _packFileService.FindFile("animations\\...\\some.anim") + }; + + _editorManager.Create(EditorEnums.MyApplet_Editor, x => (x as MyAppletViewModel)?.SetDebugInputParameters(input)); + } + } +} +``` + +This is useful for quickly launching your editor preloaded with assets. + +5) Build and run + +- Build the solution and run the host application. +- The editor toolbar should show your applet (if you used `.AddToToolbar(...)`). +- Use the toolbar button or the editor manager to create your editor. + +Troubleshooting +- If constructor dependencies are missing, ensure the required services are registered in other DI containers that the top-level bootstrapping includes (the project has many specialized DI containers; the Animation DI container depends on other shared containers). +- If your editor throws NREs at startup related to missing controllers, follow the pattern in `MountAnimationCreatorViewModel` and create default Rider/Mount/NewAnim objects in the constructor so dependent controllers are created immediately. + +Extra: register a custom view +- If you create a custom `UserControl` for your applet (e.g. `MyAppletView`), register it (AddTransient) in the DI container and change the EditorInfo registration to: + +```csharp +EditorInfoBuilder + .Create(EditorEnums.MyApplet_Editor) + .AddToToolbar("My Applet", true) + .Build(database); +``` + +That's it — you should now be able to add and iterate on an applet using the same patterns as the repo's keyframe and mount editors. diff --git a/Editors/AnimationEditor/AnimationKeyframeEditor/AnimationKeyframeEditorViewModel.cs b/Editors/AnimationEditor/AnimationKeyframeEditor/AnimationKeyframeEditorViewModel.cs index 41b4871fe..14021aca5 100644 --- a/Editors/AnimationEditor/AnimationKeyframeEditor/AnimationKeyframeEditorViewModel.cs +++ b/Editors/AnimationEditor/AnimationKeyframeEditor/AnimationKeyframeEditorViewModel.cs @@ -4,6 +4,7 @@ using System.Windows.Forms; using AnimationEditor.AnimationKeyframeEditor; using AnimationEditor.MountAnimationCreator.ViewModels; +using CommunityToolkit.Mvvm.ComponentModel; using Editors.Shared.Core.Common; using Editors.Shared.Core.Common.AnimationPlayer; using Editors.Shared.Core.Common.BaseControl; @@ -15,6 +16,7 @@ using GameWorld.Core.Components.Selection; using GameWorld.Core.Services; using Microsoft.Xna.Framework; +using Shared.Core.Events; using Shared.Core.Misc; using Shared.Core.PackFiles; using Shared.Core.Services; @@ -25,9 +27,10 @@ namespace Editors.AnimationVisualEditors.AnimationKeyframeEditor { - public class AnimationKeyframeEditorViewModel : NotifyPropertyChangedImpl, IHostedEditor + public partial class AnimationKeyframeEditorViewModel : EditorHostBase, IDisposable { - public Type EditorViewModelType => typeof(EditorView); + public override Type EditorViewModelType => typeof(EditorView); + private readonly SceneObjectViewModelBuilder _sceneObjectViewModelBuilder; private readonly AnimationPlayerViewModel _animationPlayerViewModel; private readonly SceneObjectEditor _sceneObjectBuilder; @@ -37,6 +40,7 @@ public class AnimationKeyframeEditorViewModel : NotifyPropertyChangedImpl, IHost private CopyPastePose _copyPastePose; private CopyPasteFromClipboardPose _copyPasteClipboardPose; private InterpolateBetweenPose _interpolateBetweenPose; + private readonly AnimationToolInput _animationToolInput; public SelectionComponent SelectionComponent { get => _selectionComponent; private set { _selectionComponent = value; } } private SelectionComponent _selectionComponent; @@ -70,29 +74,22 @@ public class AnimationKeyframeEditorViewModel : NotifyPropertyChangedImpl, IHost public NotifyAttr EnableInverseKinematics { get; set; } = new NotifyAttr(false); public NotifyAttr IncrementFrameAfterCopyOperation { get; set; } = new NotifyAttr(false); - public bool CopyMoreThanSingleFrame + [ObservableProperty] + private bool _copyMoreThanSingleFrame; + partial void OnCopyMoreThanSingleFrameChanged(bool value) { - get => _copyMoreThanSingleFrame; - set - { - SetAndNotify(ref _copyMoreThanSingleFrame, value); - EnableFrameNrStartTextboxOnPaste.Value = value && PasteUsingFormBelow; - } + EnableFrameNrStartTextboxOnPaste.Value = value && PasteUsingFormBelow; } - bool _copyMoreThanSingleFrame = false; public NotifyAttr DontWarnDifferentSkeletons { get; set; } = new(false); public NotifyAttr DontWarnIncomingFramesBigger { get; set; } = new(false); - public bool PasteUsingFormBelow + + [ObservableProperty] + private bool _pasteUsingFormBelow; + partial void OnPasteUsingFormBelowChanged(bool value) { - get => _pasteUsingFormBelow; - set - { - SetAndNotify(ref _pasteUsingFormBelow, value); - EnableFrameNrStartTextboxOnPaste.Value = !value && CopyMoreThanSingleFrame; - } + EnableFrameNrStartTextboxOnPaste.Value = !value && CopyMoreThanSingleFrame; } - bool _pasteUsingFormBelow = false; public NotifyAttr PastePosition { get; set; } = new(true); public NotifyAttr PasteRotation { get; set; } = new(true); @@ -106,60 +103,35 @@ public bool PasteUsingFormBelow public NotifyAttr CurrentFrameNumber { get; set; } = new(""); public NotifyAttr TotalFrameNumber { get; set; } = new(""); - public string FrameNrLength { get => _frameNrLength; set => SetAndNotify(ref _frameNrLength, value); } + [ObservableProperty] private string _frameNrLength = "0"; - public string FramesDurationInSeconds - { - get => _txtEditDurationInSeconds; - set - { - SetAndNotify(ref _txtEditDurationInSeconds, value); - } - } - private string _txtEditDurationInSeconds = ""; + [ObservableProperty] + private string _framesDurationInSeconds = ""; + + public NotifyAttr SelectedFrameAInterpolation { get; set; } = new("Not set"); public NotifyAttr SelectedFrameBInterpolation { get; set; } = new("Not set"); - public bool PreviewInterpolation - { - get => _previewInterpolation; - set - { - SetAndNotify(ref _previewInterpolation, value); - } - } + [ObservableProperty] private bool _previewInterpolation; - public bool InterpolationOnlyOnSelectedBones - { - get => _interpolationOnlyOnSelectedBones; - set - { - SetAndNotify(ref _interpolationOnlyOnSelectedBones, value); - } - } + [ObservableProperty] private bool _interpolationOnlyOnSelectedBones; - public float InterpolationValue - { - get => _interpolationValue; - set - { - SetAndNotify(ref _interpolationValue, value); - if (PreviewInterpolation) ApplyInterpolationOnCurrentFrame(); - } - } - private float _interpolationValue; - + public NotifyAttr InterpolationValue { get; set; } = new NotifyAttr(0f); public FilterCollection ModelBoneListForIKEndBone { get; set; } = new FilterCollection(null); public AnimationSettingsViewModel AnimationSettings { get; set; } = new AnimationSettingsViewModel(); - public FilterCollection SelectedRiderBone { get; set; } + public FilterCollection SelectedRiderBone { get; set; } public FilterCollection ActiveOutputFragment { get; set; } - public AnimationKeyframeEditorViewModel(IPackFileService pfs, + private readonly IEventHub _eventHub; + + public AnimationKeyframeEditorViewModel( + IEditorHostParameters editorHostParameters, + IPackFileService pfs, ISkeletonAnimationLookUpHelper skeletonAnimationLookUpHelper, SelectionComponent selectionComponent, SceneObjectViewModelBuilder sceneObjectViewModelBuilder, @@ -169,14 +141,17 @@ public AnimationKeyframeEditorViewModel(IPackFileService pfs, SelectionManager selectionManager, GizmoComponent gizmoComponent, CommandExecutor commandExecutor, - IFileSaveService packFileSaveService) + IFileSaveService packFileSaveService, + IEventHub eventHub, + AnimationToolInput animationToolInput) : base(editorHostParameters) { + DisplayName = "Keyframing Editor"; + _sceneObjectViewModelBuilder = sceneObjectViewModelBuilder; _animationPlayerViewModel = animationPlayerViewModel; _sceneObjectBuilder = sceneObjectBuilder; _pfs = pfs; - _skeletonAnimationLookUpHelper = skeletonAnimationLookUpHelper; _selectionComponent = selectionComponent; _commandFactory = commandFactory; @@ -194,6 +169,33 @@ public AnimationKeyframeEditorViewModel(IPackFileService pfs, ActiveFragmentSlot = new FilterCollection(null, (x) => UpdateCanSaveAndPreviewStates()); ActiveFragmentSlot.SearchFilter = (value, rx) => { return rx.Match(value.SlotName).Success; }; + _eventHub = eventHub; + + _eventHub.Register(this, OnSceneObjectUpdated); + + _animationToolInput = animationToolInput; + + Initialize(); + } + + void Initialize() + { + var riderItem = _sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Rider", Color.Black, _animationToolInput); + var mountItem = _sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Mount", Color.Black, _animationToolInput); + mountItem.Data.IsSelectable = true; + + var newAnimItem = _sceneObjectBuilder.CreateAsset("IDK", "New Anim", Color.Red); + _animationPlayerViewModel.RegisterAsset(newAnimItem); + + Create(riderItem.Data, mountItem.Data, newAnimItem); + SceneObjects.Add(riderItem); + SceneObjects.Add(mountItem); + //SceneObjects.Add(newAnimItem); + + _gizmoToolbox = new(this); + _copyPastePose = new(this); + _copyPasteClipboardPose = new(this); + _interpolateBetweenPose = new(this); } private void OutputAnimationSetSelected(IAnimationBinGenericFormat newValue) @@ -208,21 +210,7 @@ private void OutputAnimationSetSelected(IAnimationBinGenericFormat newValue) public void Initialize(EditorHost owner) { - //var riderItem = _sceneObjectViewModelBuilder.CreateAsset(true, "Rider", Color.Black, null); - //var mountItem = _sceneObjectViewModelBuilder.CreateAsset(true, "Mount", Color.Black, null); - //mountItem.Data.IsSelectable = true; - // - //var propAsset = _sceneObjectBuilder.CreateAsset("New Anim", Color.Red); - //_animationPlayerViewModel.RegisterAsset(propAsset); - // - //Create(riderItem.Data, mountItem.Data, propAsset); - //owner.SceneObjects.Add(riderItem); - //owner.SceneObjects.Add(mountItem); - // - //_gizmoToolbox = new(this); - //_copyPastePose = new(this); - //_copyPasteClipboardPose = new(this); - //_interpolateBetweenPose = new(this); + // Legacy init kept for compatibility; new Initialize() is used in ctor } internal void Create(SceneObject rider, SceneObject mount, SceneObject newAnimation) @@ -264,18 +252,18 @@ private void MountSkeletonChanged(GameSkeleton newValue) private void TryReGenerateAnimation(AnimationClip newValue = null) { - // if (_newAnimation != null) - // _sceneObjectBuilder.SetAnimation(_newAnimation, null); - // - // if (newValue != null) - // { - // _originalClip = newValue.Clone(); - // FramesDurationInSeconds = _originalClip.PlayTimeInSec.ToString(); - // SetFrameLengthMax(); - // } - // - // IsDirty.Value = false; - // _interpolateBetweenPose.Reset(); + // if (_newAnimation != null) + // _sceneObjectBuilder.SetAnimation(_newAnimation, null); + // + // if (newValue != null) + // { + // _originalClip = newValue.Clone(); + // FramesDurationInSeconds = _originalClip.PlayTimeInSec.ToString(); + // SetFrameLengthMax(); + // } + // + // IsDirty.Value = false; + // _interpolateBetweenPose.Reset(); } private void RiderSkeletonChanges(GameSkeleton newValue) @@ -313,6 +301,15 @@ private void RiderSkeletonChanges(GameSkeleton newValue) _skeleton = newValue; } + private void OnSceneObjectUpdated(SceneObjectUpdateEvent e) + { + // Keep player frame sync if needed; extend later for mesh/skeleton updates + if (e.Owner == _rider && (e.SkeletonChanged || e.AnimationChanged)) + RiderSkeletonChanges(_rider.Skeleton); + if (e.Owner == _mount && e.SkeletonChanged) + MountSkeletonChanged(_mount.Skeleton); + } + public List GetSelectedBones() => _gizmoToolbox.PreviousSelectedBones; public List GetModifiedBones() => _gizmoToolbox.ModifiedBones; @@ -673,7 +670,7 @@ public void ResetInterpolationTool() public void ResetInterpolationSlider() { PreviewInterpolation = false; - InterpolationValue = 0; + InterpolationValue.Value = 0; } public void ApplyInterpolationOnCurrentFrame() @@ -722,5 +719,10 @@ public void SaveAs() _packFileSaveService.SaveAs(".anim", bytes); IsDirty.Value = false; } + + public void Dispose() + { + _eventHub?.UnRegister(this); + } } } diff --git a/Editors/AnimationEditor/AnimationKeyframeEditor/InterpolateBetweenPose.cs b/Editors/AnimationEditor/AnimationKeyframeEditor/InterpolateBetweenPose.cs index d40d4ba26..4cb843045 100644 --- a/Editors/AnimationEditor/AnimationKeyframeEditor/InterpolateBetweenPose.cs +++ b/Editors/AnimationEditor/AnimationKeyframeEditor/InterpolateBetweenPose.cs @@ -102,7 +102,7 @@ private void ApplyOnCurrentFrame() _keyframeNrA, _keyframeNrB, _parent.Skeleton, - _parent.InterpolationValue, + _parent.InterpolationValue.Value, _parent.PastePosition.Value, _parent.PasteRotation.Value, _parent.PasteScale.Value)).BuildAndExecute(); @@ -119,7 +119,7 @@ private void ApplyOnCurrentFrameSelectedBones() _keyframeNrA, _keyframeNrB, _parent.Skeleton, - _parent.InterpolationValue, + _parent.InterpolationValue.Value, _parent.GetSelectedBones(), _parent.PastePosition.Value, _parent.PasteRotation.Value, diff --git a/Editors/AnimationEditor/DependencyInjectionContainer.cs b/Editors/AnimationEditor/DependencyInjectionContainer.cs index 0d645f31f..6b2a45d56 100644 --- a/Editors/AnimationEditor/DependencyInjectionContainer.cs +++ b/Editors/AnimationEditor/DependencyInjectionContainer.cs @@ -13,10 +13,23 @@ public class DependencyInjectionContainer : DependencyContainer { public override void Register(IServiceCollection serviceCollection) { - serviceCollection.AddScoped>(); + + //serviceCollection.AddScoped(); + //serviceCollection.AddScoped(); + //serviceCollection.AddScoped(); + //serviceCollection.AddScoped(); + //serviceCollection.AddScoped(); + + //RegisterWindow(serviceCollection); + //RegisterWindow(serviceCollection); + + serviceCollection.AddScoped(); + + //serviceCollection.AddScoped>(); + //serviceCollection.AddScoped(); serviceCollection.AddScoped(); - serviceCollection.AddScoped>(); + // Use the new EditorHostBase-based view model directly for the Keyframe editor serviceCollection.AddScoped(); RegisterAllAsInterface(serviceCollection, ServiceLifetime.Transient); @@ -25,13 +38,14 @@ public override void Register(IServiceCollection serviceCollection) public override void RegisterTools(IEditorDatabase database) { EditorInfoBuilder - .Create, EditorHostView>(EditorEnums.MountTool_Editor) - .AddToToolbar("Mount Tool", false) + .Create(EditorEnums.MountTool_Editor) + .AddToToolbar("Mount Tool", true) .Build(database); - + + // Register the Keyframe editor using its EditorHostBase-based type EditorInfoBuilder - .Create, EditorHostView>(EditorEnums.AnimationKeyFrame_Editor) - .AddToToolbar("KeyFrame Tool", false) + .Create(EditorEnums.AnimationKeyFrame_Editor) + .AddToToolbar("KeyFrame Tool", true) .Build(database); } } diff --git a/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs b/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs index 3818c7eac..9a1124c17 100644 --- a/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs +++ b/Editors/AnimationEditor/MountAnimationCreator/DevConfig/MountTool.cs @@ -42,7 +42,7 @@ static void CreateLionAndHu01b(IEditorManager creator, IPackFileService packfile Mesh = packfileService.FindFile(@"variantmeshes\variantmeshdefinitions\hef_war_lion.variantmeshdefinition") }; - creator.Create(EditorEnums.MountTool_Editor, x => (x as EditorHost).Editor.SetDebugInputParameters(riderInput, mountInput)); + creator.Create(EditorEnums.MountTool_Editor, x => (x as MountAnimationCreatorViewModel)?.SetDebugInputParameters(riderInput, mountInput)); } } } diff --git a/Editors/AnimationEditor/MountAnimationCreator/MountAnimationCreatorViewModel.cs b/Editors/AnimationEditor/MountAnimationCreator/MountAnimationCreatorViewModel.cs index eec1d6793..a1031a1a4 100644 --- a/Editors/AnimationEditor/MountAnimationCreator/MountAnimationCreatorViewModel.cs +++ b/Editors/AnimationEditor/MountAnimationCreator/MountAnimationCreatorViewModel.cs @@ -31,9 +31,10 @@ namespace AnimationEditor.MountAnimationCreator { - public class MountAnimationCreatorViewModel : NotifyPropertyChangedImpl, IHostedEditor + public partial class MountAnimationCreatorViewModel : EditorHostBase { - public Type EditorViewModelType => typeof(EditorView); + public override Type EditorViewModelType => typeof(EditorView); + private readonly SceneObjectViewModelBuilder _sceneObjectViewModelBuilder; private readonly SceneObjectEditor _sceneObjectBuilder; private readonly IFileSaveService _fileSaveService; @@ -42,20 +43,21 @@ public class MountAnimationCreatorViewModel : NotifyPropertyChangedImpl, IHosted private readonly IPackFileService _pfs; private readonly SelectionManager _selectionManager; private readonly ISkeletonAnimationLookUpHelper _skeletonAnimationLookUpHelper; + private readonly IGlobalEventHub _globalEventHub; SceneObject _mount; SceneObject _rider; SceneObject _newAnimation; - + List _mountVertexes = new(); Rmv2MeshNode _mountVertexOwner; AnimationToolInput _inputRiderData; AnimationToolInput _inputMountData; + bool _initializedFromDebugInputs = false; public AnimationSettingsViewModel AnimationSettings { get; set; } = new AnimationSettingsViewModel(); public MountLinkViewModel MountLinkController { get; set; } - public string EditorName => "Mount Animation Creator"; public FilterCollection SelectedRiderBone { get; set; } @@ -76,15 +78,20 @@ public class MountAnimationCreatorViewModel : NotifyPropertyChangedImpl, IHosted public FilterCollection ActiveOutputFragment { get; set; } public FilterCollection ActiveFragmentSlot { get; set; } - public MountAnimationCreatorViewModel(IPackFileService pfs, - ISkeletonAnimationLookUpHelper skeletonAnimationLookUpHelper, + public MountAnimationCreatorViewModel( + IEditorHostParameters editorHostParameters, + IPackFileService pfs, + ISkeletonAnimationLookUpHelper skeletonAnimationLookUpHelper, SelectionManager selectionManager, SceneObjectViewModelBuilder sceneObjectViewModelBuilder, AnimationPlayerViewModel animationPlayerViewModel, SceneObjectEditor sceneObjectBuilder, IFileSaveService fileSaveService, - IUiCommandFactory uiCommandFactory) + IUiCommandFactory uiCommandFactory, + IGlobalEventHub globalEventHub) : base(editorHostParameters) { + DisplayName = "Mount Animation Creator"; + _sceneObjectViewModelBuilder = sceneObjectViewModelBuilder; _animationPlayerViewModel = animationPlayerViewModel; _sceneObjectBuilder = sceneObjectBuilder; @@ -94,9 +101,10 @@ public MountAnimationCreatorViewModel(IPackFileService pfs, _skeletonAnimationLookUpHelper = skeletonAnimationLookUpHelper; _selectionManager = selectionManager; + _globalEventHub = globalEventHub; - DisplayGeneratedSkeleton = new NotifyAttr(true, (value) => _newAnimation.ShowSkeleton.Value = value); - DisplayGeneratedMesh = new NotifyAttr(true, (value) => { if (_newAnimation.MainNode != null) _newAnimation.ShowMesh.Value = value; }); + DisplayGeneratedSkeleton = new NotifyAttr(true, (value) => { if (_newAnimation != null) _newAnimation.ShowSkeleton.Value = value; }); + DisplayGeneratedMesh = new NotifyAttr(true, (value) => { if (_newAnimation?.MainNode != null) _newAnimation.ShowMesh.Value = value; }); SelectedRiderBone = new FilterCollection(null, (x) => UpdateCanSaveAndPreviewStates()); @@ -108,26 +116,70 @@ public MountAnimationCreatorViewModel(IPackFileService pfs, ActiveFragmentSlot.SearchFilter = (value, rx) => { return rx.Match(value.SlotName).Success; }; AnimationSettings.SettingsChanged += () => TryReGenerateAnimation(null); + + // If debug inputs were provided before ctor finished, initialize now + if (_inputRiderData != null && _inputMountData != null) + { + InitializeFromDebugInputs(); + } + else + { + // Default initialization when running inside the app (no debug inputs provided). + // This mirrors other editors (eg. AnimationKeyframeEditor) and ensures + // MountLinkController and scene objects are created so the view model + // is fully usable after the applet loads. + var riderItem = _sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Rider", Color.Black, null); + var mountItem = _sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Mount", Color.Black, null); + mountItem.Data.IsSelectable = true; + + var propAsset = _sceneObjectBuilder.CreateAsset("IDK", "New Anim", Color.Red); + _animationPlayerViewModel.RegisterAsset(propAsset); + + Create(riderItem.Data, mountItem.Data, propAsset); + SceneObjects.Add(riderItem); + SceneObjects.Add(mountItem); + } } public void SetDebugInputParameters(AnimationToolInput rider, AnimationToolInput mount) + { + _inputRiderData = rider; + _inputMountData = mount; + + + + // Only initialize from debug inputs if we haven't already initialized the view model. + // This prevents creating duplicate scene objects when the app performs the default init in the ctor. + if (!_initializedFromDebugInputs && MountLinkController == null) + InitializeFromDebugInputs(); + + } + + void InitializeFromDebugInputs() + { + if (_initializedFromDebugInputs) return; + if (_inputRiderData == null || _inputMountData == null) return; + + var riderItem = _sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Rider", Color.Black, _inputRiderData); + var mountItem = _sceneObjectViewModelBuilder.CreateAsset("IDK", true, "Mount", Color.Black, _inputMountData); + mountItem.Data.IsSelectable = true; + + var propAsset = _sceneObjectBuilder.CreateAsset("IDK", "New Anim", Color.Red); + _animationPlayerViewModel.RegisterAsset(propAsset); + + Create(riderItem.Data, mountItem.Data, propAsset); + SceneObjects.Add(riderItem); + SceneObjects.Add(mountItem); + + _initializedFromDebugInputs = true; } public void Initialize(EditorHost owner) { - // var riderItem = _sceneObjectViewModelBuilder.CreateAsset(true, "Rider", Color.Black, _inputRiderData); - // var mountItem = _sceneObjectViewModelBuilder.CreateAsset(true, "Mount", Color.Black, _inputMountData); - // mountItem.Data.IsSelectable = true; - // - // var propAsset = _sceneObjectBuilder.CreateAsset("New Anim", Color.Red); - // _animationPlayerViewModel.RegisterAsset(propAsset); - // - // Create(riderItem.Data, mountItem.Data, propAsset); - // owner.SceneObjects.Add(riderItem); - // owner.SceneObjects.Add(mountItem); + // Legacy init kept for compatibility; new initialization is handled in ctor or SetDebugInputParameters } internal void Create(SceneObject rider, SceneObject mount, SceneObject newAnimation) @@ -149,14 +201,14 @@ internal void Create(SceneObject rider, SceneObject mount, SceneObject newAnimat private void TryReGenerateAnimation(AnimationClip newValue = null) { - // UpdateCanSaveAndPreviewStates(); - // if (CanPreview.Value) - // CreateMountAnimationAction(); - // else - // { - // if (_newAnimation != null) - // _sceneObjectBuilder.SetAnimation(_newAnimation, null); - // } + // UpdateCanSaveAndPreviewStates(); + // if (CanPreview.Value) + // CreateMountAnimationAction(); + // else + // { + // if (_newAnimation != null) + _sceneObjectBuilder.SetAnimation(_newAnimation, null); + // } } private void MountSkeletonChanged(GameSkeleton newValue) @@ -232,14 +284,14 @@ public void SetMountVertex() public void CreateMountAnimationAction() { - //var newRiderAnim = CreateAnimationGenerator().GenerateMountAnimation(_mount.AnimationClip, _rider.AnimationClip); - // - //// Apply - //_sceneObjectBuilder.CopyMeshFromOther(_newAnimation, _rider); - //_sceneObjectBuilder.SetAnimationClip(_newAnimation, newRiderAnim, new SkeletonAnimationLookUpHelper.AnimationReference("Generated animation", null)); - //_newAnimation.ShowSkeleton.Value = DisplayGeneratedSkeleton.Value; - //_newAnimation.ShowMesh.Value = DisplayGeneratedMesh.Value; - //UpdateCanSaveAndPreviewStates(); + var newRiderAnim = CreateAnimationGenerator().GenerateMountAnimation(_mount.AnimationClip, _rider.AnimationClip); + // + //// Apply + _sceneObjectBuilder.CopyMeshFromOther(_newAnimation, _rider); + _sceneObjectBuilder.SetAnimationClip(_newAnimation, newRiderAnim, "Gemerated anim"); + _newAnimation.ShowSkeleton.Value = DisplayGeneratedSkeleton.Value; + _newAnimation.ShowMesh.Value = DisplayGeneratedMesh.Value; + UpdateCanSaveAndPreviewStates(); } MountAnimationGeneratorService CreateAnimationGenerator() @@ -274,7 +326,7 @@ public void AddAnimationToFragment() //var bytes = AnimationPackSerializer.ConvertToBytes(ActiveOutputFragment.SelectedItem.Parent); //SaveHelper.Save(_pfs, "animations\\animation_tables\\" + ActiveOutputFragment.SelectedItem.Parent.FileName, null, bytes, false); // - //// Update status for the slot thing + //// Update status for the slot thing //var possibleValues = ActiveOutputFragment.SelectedItem.Fragments.Select(x => new FragmentStatusSlotItem(x)); //ActiveFragmentSlot.UpdatePossibleValues(possibleValues); //MountLinkController.ReloadFragments(true, false); @@ -301,8 +353,8 @@ public void RefreshViewAction() public void SaveCurrentAnimationAction() { - // var service = new BatchProcessorService(_pfs, _skeletonAnimationLookUpHelper, CreateAnimationGenerator(), new BatchProcessOptions { SavePrefix = SavePrefixText.Value }, _fileSaveService, SelectedAnimationOutputFormat.Value); - // service.SaveSingleAnim(_mount.AnimationClip, _rider.AnimationClip, _rider.AnimationName.Value.AnimationFile); + // var service = new BatchProcessorService(_pfs, _skeletonAnimationLookUpHelper, CreateAnimationGenerator(), new BatchProcessOptions { SavePrefix = SavePrefixText.Value }, _fileSaveService, SelectedAnimationOutputFormat.Value); + // service.SaveSingleAnim(_mount.AnimationClip, _rider.AnimationClip, _rider.AnimationName.Value.AnimationFile); } public void BatchProcessUsingFragmentsAction() @@ -342,6 +394,3 @@ public void CopyAnimation() } } } - - - diff --git a/Editors/DatabaseEditor/Utility.DatabaseSchemaGenerator/Examples/DbSchemaBuilder.cs b/Editors/DatabaseEditor/Utility.DatabaseSchemaGenerator/Examples/DbSchemaBuilder.cs new file mode 100644 index 000000000..67e2379ca --- /dev/null +++ b/Editors/DatabaseEditor/Utility.DatabaseSchemaGenerator/Examples/DbSchemaBuilder.cs @@ -0,0 +1,364 @@ +using System.Data; +using System.Data.SqlClient; +using System.Data.SQLite; +using System.Linq; +using Editors.DatabaseEditor.FileFormats; +using Shared.Core.ByteParsing; +using Shared.Core.PackFiles; +using Shared.Core.Settings; + +namespace Utility.DatabaseSchemaGenerator.Examples +{ + internal class DbSchemaBuilder + { + // battle_skeletons_tables + // battle_skeleton_parts_tables + + + + /* + CREATE TABLE IF NOT EXISTS Products ( + Id INTEGER, + Name TEXT NOT NULL, + Price REAL NOT NULL + ) WITHOUT ROWID; + */ + + + private readonly static Dictionary s_mappingTable = new() + { + {"StringU8", "TEXT NOT NULL"}, + {"StringU16", "TEXT NOT NULL"}, + {"OptionalStringU8", "TEXT"}, + {"F32","FLOAT"}, + {"F64","REAL"}, + + {"Boolean","bit"}, + + {"I32","INT"}, + {"I64","BIGINT"}, + {"I16","smallint"}, + {"OptionalI32","NullableColumn INTEGER"}, + {"ColourRGB","tinyint"},// probably need to find a better type + + }; + + + private readonly static List<(string SchemaName, string sqlType, DbType SqlSerializeType, DbTypesEnum GameType)> s_typeMappingTable = new() + { + {("StringU8", "TEXT NOT NULL", DbType.String, DbTypesEnum.String)}, + {("OptionalStringU8", "TEXT", DbType.String, DbTypesEnum.Optstring)}, + + {("StringU16", "TEXT NOT NULL", DbType.AnsiString, DbTypesEnum.String_ascii)}, + {("OptionalStringU8", "TEXT", DbType.AnsiString, DbTypesEnum.Optstring_ascii)}, + + {("F32", "FLOAT", DbType.Double, DbTypesEnum.Single)}, + {("F64", "REAL", DbType.Double, DbTypesEnum.Int64)}, // wrong type + + {("I16", "smallint", DbType.Int16, DbTypesEnum.Short)}, + {("I32", "INT", DbType.Int32, DbTypesEnum.Integer)}, + {("OptionalI32", "NullableColumn INTEGER", DbType.Int32, DbTypesEnum.Integer)}, // wrong type + {("I64", "BIGINT", DbType.Int64, DbTypesEnum.Int64)}, + + {("Boolean", "TEXT NOT NULL", DbType.Boolean, DbTypesEnum.Boolean)}, + + {("ColourRGB", "tinyint", DbType.SByte, DbTypesEnum.Short)}, // wrong type + }; + + private readonly DbSchema _jsonSchema; + private readonly IPackFileService _packFileService; + + public DbSchemaBuilder(IPackFileService packFileService) + { + _jsonSchema = DbScehmaParser.CreateFromRpfmJson(@"C:\Users\ole_k\Downloads\schema_wh3.json", GameTypeEnum.Warhammer3); + + // Filter scheamas to make debugging easier + _jsonSchema.TableSchemas = _jsonSchema.TableSchemas + // Make sure we grab latest version (For now) + .OrderByDescending(x=>x.Version) + .GroupBy(x => x.Name) + .Select(x=>x.First()) + + // For debugging, only have a few tables + //.Where(x => x.Name == "battle_skeleton_parts_tables" || x.Name == "battle_skeletons_tables") + .ToList(); + + + + DbScemaValidation.Validate(_jsonSchema); + _packFileService = packFileService; + } + + public void CreateSqTableScehma(SQLiteConnection connection, bool addKeys, bool addForeignKeys) + { + var numEdges = 0; + var numNodes = 0; + var maxEdges = 10; + var tablesWithoutFk = 0; + var tableSchemas = _jsonSchema.TableSchemas; + + + + + var tables = new Dictionary(); + foreach (var tableSchema in tableSchemas) + { + var tableName = tableSchema.Name; + numNodes++; + + var sqlCommand = $"CREATE TABLE {tableName} (\n"; + for(var i = 0; i < tableSchema.Coloumns.Count; i++) + { + var coloumnSchema = tableSchema.Coloumns[i]; + var coloumName = coloumnSchema.Name; + var coloumDataType = s_mappingTable[coloumnSchema.DataType]; + var coloumnModifier = ""; + var comma = i != (tableSchema.Coloumns.Count-1) ? "," : ""; + + sqlCommand += $"\t[{coloumName}] {coloumDataType} {coloumnModifier} {comma} \n"; + } + + var foreignKeyColoums = tableSchema.Coloumns.Where(x=>x.ForeignKey != null).ToList(); + if (foreignKeyColoums.Count == 0) + tablesWithoutFk++; + + if (addForeignKeys) + { + foreach (var coloumn in foreignKeyColoums) + { + var coloumName = coloumn.Name; + var foreignKeyTableName = coloumn.ForeignKey.Table + "_tables"; + var foreignTableRef = tableSchemas.FirstOrDefault(x => x.Name == foreignKeyTableName); + if (foreignTableRef != null) + { + sqlCommand += $"\t ,FOREIGN KEY ([{coloumName}]) REFERENCES {foreignKeyTableName}([{coloumn.ForeignKey.ColoumnName}]) \n"; + numEdges++; + } + } + } + + var keyColoumns = tableSchema.Coloumns + .Where(x=>x.IsKey) + .Select(x=>$"[{x.Name}]") + .ToList(); + + // Add this to the top to make it look prettier + if (keyColoumns.Any()) + sqlCommand += $",PRIMARY KEY ({string.Join(", ", keyColoumns)})"; + + sqlCommand += ");"; + + tables.Add(tableName, sqlCommand); + } + + var fullSqlCommand = ""; + fullSqlCommand +=string.Join("\n\n", tables.Values); + + // Execute the schema SQL commands + using var command = new SQLiteCommand(fullSqlCommand, connection); + + command.ExecuteNonQuery(); + } + + + + DataTable? LoadTable(string tableName) + { + + var tableSchema = _jsonSchema.TableSchemas.First(x=>x.Name == tableName); + + var packFile = _packFileService.FindFile($"db\\{tableName}\\data__"); + if (packFile == null) + { + Console.WriteLine(" - Not found"); + return null; + } + var byteChunk = packFile.DataSource.ReadDataAsChunk(); + + var peakGuid = byteChunk.PeakUint32(); + if ((uint)4294770429 == peakGuid) + { + byteChunk.ReadUInt32(); + var guid = byteChunk.ReadStringAscii(); + } + + var peakVersion = byteChunk.PeakUint32(); + if ((uint)4294901244 == peakVersion) + { + byteChunk.ReadUInt32(); + var version = byteChunk.ReadUInt32(); + } + + var unkownBool = byteChunk.ReadBool(); + var numTableEntries = byteChunk.ReadUInt32(); + + // Build the parameters + var dt = new DataTable(); + dt.Clear(); + foreach (var tableColoumn in tableSchema.Coloumns) + dt.Columns.Add(tableColoumn.Name); + + // Fill the command with game data + for (var i = 0; i < numTableEntries; i++) + { + var row = dt.NewRow(); + for (var j = 0; j < tableSchema.Coloumns.Count; j++) + { + var coloumn = tableSchema.Coloumns[j]; + + var gameType = s_typeMappingTable.First(x => x.SchemaName == coloumn.DataType).GameType; + var valueFromGameDb = byteChunk.ReadObject(gameType); + row[coloumn.Name] = valueFromGameDb; + } + + } + + if (byteChunk.BytesLeft != 0) + throw new Exception("Data left - error parsing"); + + return dt; + } + + public void PopulateTable(IPackFileService packFileService, SQLiteConnection sqlConnection) + { + var tableSchemas = _jsonSchema.TableSchemas; + var parsedTables = 0; + var skippedTables = 0; + var failedTables = new List(); + + var tables = new List(); + foreach (var tableSchema in tableSchemas) + { + try + { + var parsedTable = LoadTable(tableSchema.Name); + if(parsedTable != null) + tables.Add(parsedTable); + } + catch (Exception e) + { + } + + } + + + + + //diplomacy_negotiation_string_options_tables + //https://timdeschryver.dev/blog/faster-sql-bulk-inserts-with-csharp#sql-bulk-copy? + + + foreach (var tableSchema in tableSchemas) + { + try + { + //using (var copy = new SqlBulkCopy(sqlConnection.ConnectionString)) + //{ + + + // //var _ravi = dt.NewRow(); + // //_ravi["Name"] = "ravi"; + // //_ravi["Marks"] = "500"; + // //dt.Rows.Add(_ravi); + // // + // ////copy.BatchSize + // //copy.DestinationTableName = "dbo.Customers"; + // //copy.ColumnMappings.Add(nameof(Customer.Id), "Id"); + // //copy.ColumnMappings.Add(nameof(Customer.FirstName), "FirstName"); + + + // // copy.WriteToServer(dt); + //} + + Console.WriteLine($"{tableSchema.Name} - {parsedTables}/{tableSchemas.Count}"); + + // Skip tables with datatypes that are currently not supported: + string[] unsupportedTypes = ["F62", "OptionalI32", "ColourRGB"]; + + var allTypes = tableSchema.Coloumns + .Select(x => x.DataType) + .Distinct() + .ToList(); + + var hasMatch = allTypes.Any(x => unsupportedTypes.Any(y => y == x)); + if (hasMatch) + { + Console.WriteLine(" - Skipped"); + skippedTables++; + continue; + } + + // PackFile Parsing + var packFile = packFileService.FindFile($"db\\{tableSchema.Name}\\data__"); + if (packFile == null) + { + Console.WriteLine(" - Not found"); + continue; + } + var byteChunk = packFile.DataSource.ReadDataAsChunk(); + + var peakGuid = byteChunk.PeakUint32(); + if ((uint)4294770429 == peakGuid) + { + byteChunk.ReadUInt32(); + var guid = byteChunk.ReadStringAscii(); + } + + var peakVersion = byteChunk.PeakUint32(); + if ((uint)4294901244 == peakVersion) + { + byteChunk.ReadUInt32(); + var version = byteChunk.ReadUInt32(); + } + + var unkownBool = byteChunk.ReadBool(); + var numTableEntries = byteChunk.ReadUInt32(); + + // Create sql command + var tableColoumnNames = tableSchema.Coloumns.Select(x => $"[{x.Name}]").ToList(); + var values = tableSchema.Coloumns.Select(x => $"@{x.Name}").ToList(); + + var strCommand = $"INSERT INTO {tableSchema.Name} ({string.Join(",", tableColoumnNames)}) VALUES ({string.Join(",", values)})"; + using var sqlCommand = new SQLiteCommand(strCommand, sqlConnection); + + // Build the parameters + var valueParameters = new List(); + foreach (var tableColoumn in tableSchema.Coloumns) + { + var dbType = s_typeMappingTable.First(x => x.SchemaName == tableColoumn.DataType).SqlSerializeType; + var dbName = $"@{tableColoumn.Name}"; + var newParam = sqlCommand.Parameters.Add(dbName, dbType); + valueParameters.Add(newParam); + } + + // Fill the command with game data + for (var i = 0; i < numTableEntries; i++) + { + //foreach (var valueParameter in valueParameters) + for (var j = 0; j < tableSchema.Coloumns.Count; j++) + { + var valueParameter = valueParameters[j]; + var coloumn = tableSchema.Coloumns[j]; + + var gameType = s_typeMappingTable.First(x => x.SchemaName == coloumn.DataType).GameType; + var valueFromGameDb = byteChunk.ReadObject(gameType); + valueParameter.Value = valueFromGameDb; + } + + sqlCommand.ExecuteNonQuery(); + } + + if (byteChunk.BytesLeft != 0) + throw new Exception("Data left - error parsing"); + + parsedTables++; + } + catch (Exception e) + { + Console.WriteLine(" - Failed"); + failedTables.Add(tableSchema.Name + " - " + e.Message); + } + } + } + } +} diff --git a/GameWorld/GameWorldCore/GameWorld.Core/WpfWindow/WpfGame.cs b/GameWorld/GameWorldCore/GameWorld.Core/WpfWindow/WpfGame.cs index 3903e6d35..a1af1bbc0 100644 --- a/GameWorld/GameWorldCore/GameWorld.Core/WpfWindow/WpfGame.cs +++ b/GameWorld/GameWorldCore/GameWorld.Core/WpfWindow/WpfGame.cs @@ -223,7 +223,8 @@ public T GetComponent() where T : IGameComponent public T AddComponent(T comp) where T : IGameComponent { - Components.Add(comp); + if (!Components.Contains(comp)) + Components.Add(comp); return comp; }