diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d313053e..782b12a8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,10 @@ jobs: with: dotnet-version: 8.0.x - name: Build local dependencies - run: bash zzio/get-dependencies.sh + run: | + cd zzio + bash ./get-dependencies.sh + cd .. - name: Install remote dependencies run: dotnet restore zzio/zzio.sln -r ${{ matrix.target }} -p:NoWarn=NU1605 - name: Build diff --git a/zzio/scn/FOModel.cs b/zzio/scn/FOModel.cs index 8b290657..eea75bdb 100644 --- a/zzio/scn/FOModel.cs +++ b/zzio/scn/FOModel.cs @@ -72,4 +72,8 @@ public void Write(Stream stream) writer.Write(useCachedModels); writer.Write(wiggleAmpl); } + public FOModel Clone() + { + return (FOModel)this.MemberwiseClone(); + } } diff --git a/zzio/scn/Scene.cs b/zzio/scn/Scene.cs index 42e4b056..e8a394bc 100644 --- a/zzio/scn/Scene.cs +++ b/zzio/scn/Scene.cs @@ -115,6 +115,11 @@ public void Write(Stream stream) writer.WriteZString("[Scenefile]"); writeSingle(writer, version, "[Version]"); writeSingle(writer, misc, "[Misc]"); + if (backdropFile.Length > 0) + { + writer.WriteZString("[Backdrop]"); + writer.WriteZString(backdropFile); + } writeArray(writer, lights, "[Lights]"); writeArray(writer, foModels, "[FOModels_v4]"); writeArray(writer, models, "[Models_v3]"); @@ -136,15 +141,8 @@ public void Write(Stream stream) writer.Write(sceneOrigin); writeArray(writer, textureProperties, "[TextureProperties]"); writeSingle(writer, waypointSystem, "[WaypointSystem]"); - - if (backdropFile.Length > 0) - { - writer.WriteZString("[Backdrop]"); - writer.WriteZString(backdropFile); - } - writeArray(writer, effects, "[Effects]"); writer.WriteZString("[EOS]"); } } } -} \ No newline at end of file +} diff --git a/zzio/scn/Trigger.cs b/zzio/scn/Trigger.cs index 6ec0df7c..3dd928c0 100644 --- a/zzio/scn/Trigger.cs +++ b/zzio/scn/Trigger.cs @@ -82,4 +82,8 @@ public void Write(Stream stream) break; } } + public Trigger Clone() + { + return (Trigger)this.MemberwiseClone(); + } } diff --git a/zzre.core/math/ZZIOExtensions.cs b/zzre.core/math/ZZIOExtensions.cs index 01248009..36073dc4 100644 --- a/zzre.core/math/ZZIOExtensions.cs +++ b/zzre.core/math/ZZIOExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using Veldrid; using zzio; @@ -28,6 +28,12 @@ public static Quaternion ToZZRotation(this Vector3 v) return Quaternion.Conjugate(Quaternion.CreateFromRotationMatrix(Matrix4x4.CreateLookAt(Vector3.Zero, v, up))); } + public static Vector3 FromZZRotation(this Quaternion q) + { + Matrix4x4 rotationMatrix = Matrix4x4.CreateFromQuaternion(Quaternion.Conjugate(q)); + return Vector3.Normalize(new Vector3(rotationMatrix.M13, rotationMatrix.M23, rotationMatrix.M33)); + } + public static Vector3 ToZZRotationVector(this Quaternion rotation) => Vector3.Transform(-Vector3.UnitZ, rotation) * -1f; @@ -45,7 +51,7 @@ public static Vector3 ToZZRotationVector(this Quaternion rotation) => }; // adapted from https://gist.github.com/ciembor/1494530 - "It's the do what you want license :)" - + public static FColor RGBToHSL(this FColor c) { float max = Max(Max(c.r, c.g), c.b); diff --git a/zzre/imgui/TwoColumnEditorTag.cs b/zzre/imgui/TwoColumnEditorTag.cs index 80e50cef..a786ddc5 100644 --- a/zzre/imgui/TwoColumnEditorTag.cs +++ b/zzre/imgui/TwoColumnEditorTag.cs @@ -37,6 +37,8 @@ public void AddInfoSection(string name, Action content, bool defaultOpen = true, public void ClearInfoSections() => infoSections.Clear(); + public void ResetColumnWidth() => didSetColumnWidth = 0; + private void HandleContent() { ImGui.Columns(2, null, true); diff --git a/zzre/tools/sceneeditor/SceneEditor.FOModel.cs b/zzre/tools/sceneeditor/SceneEditor.FOModel.cs index 792d83a0..eb8da23b 100644 --- a/zzre/tools/sceneeditor/SceneEditor.FOModel.cs +++ b/zzre/tools/sceneeditor/SceneEditor.FOModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Numerics; using Veldrid; @@ -34,6 +35,12 @@ private sealed class FOModel : BaseDisposable, ISelectable public IRaycastable RenderedBounds => SelectableBounds; public float ViewSize => mesh.BoundingBox.MaxSizeComponent; + public void SyncWithScene() + { + SceneFOModel.pos = Location.LocalPosition; + SceneFOModel.rot = Location.LocalRotation.FromZZRotation(); + } + public FOModel(ITagContainer diContainer, zzio.scn.FOModel sceneModel) { this.diContainer = diContainer; @@ -174,6 +181,12 @@ public FOModelComponent(ITagContainer diContainer) editor.editor.AddInfoSection("FOModels", HandleInfoSection, false); } + public void SyncWithScene() + { + foreach (var foModel in models) + foModel.SyncWithScene(); + } + protected override void DisposeManaged() { base.DisposeManaged(); @@ -189,7 +202,19 @@ private void HandleLoadScene() if (editor.scene == null) return; - models = editor.scene.foModels.Select(m => new FOModel(diContainer, m)).ToArray(); + var list = new List(); + foreach (var m in editor.scene.foModels) + { + try + { + list.Add(new FOModel(diContainer, m)); + } + catch (InvalidDataException) + { + Console.Error.WriteLine("Fail to load FOModel with ID {0}, ignoring file {1}", m.idx, m.filename); + } + } + models = [.. list]; } private void HandleRender(CommandList cl) @@ -221,7 +246,54 @@ private void HandleInfoSection() TreePop(); } } + private FOModel? FindCurrentFoModel() + { + foreach (var foModel in models) + { + if (foModel == editor.Selected) + return foModel; + } + return null; + } + private uint GetNextAvailableFoModelID() + { + uint result = 1; + foreach (var foModel in models) + { + result = Math.Max(result, foModel.SceneFOModel.idx); + } + return result + 1; + } + public void DeleteCurrentFoModel() + { + var currentFoModel = FindCurrentFoModel(); + if (currentFoModel == null || editor.scene == null) + return; + + SyncWithScene(); + + + editor.scene.foModels = editor.scene.foModels.Where( + model => model.idx != currentFoModel.SceneFOModel.idx + ).ToArray(); + HandleLoadScene(); + editor.Selected = null; + } + public void DuplicateCurrentFoModel() + { + var currentFoModel = FindCurrentFoModel(); + if (currentFoModel == null || editor.scene == null) + return; + + SyncWithScene(); + + var copy = currentFoModel.SceneFOModel.Clone(); + copy.idx = GetNextAvailableFoModelID(); + editor.scene.foModels = [.. editor.scene.foModels, copy]; + HandleLoadScene(); + editor.Selected = models.Last(); + } IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)models).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => models.Cast().GetEnumerator(); } diff --git a/zzre/tools/sceneeditor/SceneEditor.ISelectable.cs b/zzre/tools/sceneeditor/SceneEditor.ISelectable.cs index 6390fb12..c61446e6 100644 --- a/zzre/tools/sceneeditor/SceneEditor.ISelectable.cs +++ b/zzre/tools/sceneeditor/SceneEditor.ISelectable.cs @@ -1,4 +1,5 @@ using ImGuizmoNET; +using ImGuiNET; using System; using System.Collections.Generic; using System.Linq; @@ -24,6 +25,7 @@ public interface ISelectable private readonly List> selectableContainers = []; private IEnumerable Selectables => selectableContainers.SelectMany(c => c); + private static OPERATION gizmoOperation = OPERATION.TRANSLATE; private ISelectable? _selected; private ISelectable? Selected { @@ -94,7 +96,9 @@ private void HandleGizmos() var projection = camera.Projection; var matrix = selected.Location.LocalToWorld; ImGuizmo.SetDrawlist(); - if (ImGuizmo.Manipulate(ref view.M11, ref projection.M11, OPERATION.TRANSLATE, MODE.LOCAL, ref matrix.M11)) + if (!ImGuizmo.IsUsing()) + gizmoOperation = ImGui.IsKeyDown(ImGuiKey.ModShift) ? OPERATION.ROTATE : OPERATION.TRANSLATE; + if (ImGuizmo.Manipulate(ref view.M11, ref projection.M11, gizmoOperation, MODE.LOCAL, ref matrix.M11)) { selected.Location.LocalToWorld = matrix; editor.TriggerSelectionManipulate(); @@ -117,7 +121,7 @@ private void HandleNewSelection(ISelectable? newSelected) private void HandleClick(MouseButton button, Vector2 pos) { - if (button != MouseButton.Left)// || ImGuizmo.IsOver() || ImGuizmo.IsUsing()) + if (button != MouseButton.Left || ImGuizmo.IsUsing()) // || ImGuizmo.IsOver() return; var ray = camera.RayAt((pos * 2f - Vector2.One) * new Vector2(1f, -1f)); diff --git a/zzre/tools/sceneeditor/SceneEditor.Trigger.cs b/zzre/tools/sceneeditor/SceneEditor.Trigger.cs index f60f8e86..380829ef 100644 --- a/zzre/tools/sceneeditor/SceneEditor.Trigger.cs +++ b/zzre/tools/sceneeditor/SceneEditor.Trigger.cs @@ -77,6 +77,11 @@ public void Content() break; } } + + public void SyncWithScene() + { + SceneTrigger.pos = Location.LocalPosition; + } } private sealed class TriggerComponent : BaseDisposable, IEnumerable @@ -90,7 +95,7 @@ private sealed class TriggerComponent : BaseDisposable, IEnumerable private Trigger[] triggers = []; private bool wasSelected; - private float iconSize = 128f; + private float iconSize = 256f; public TriggerComponent(ITagContainer diContainer) { @@ -112,6 +117,11 @@ public TriggerComponent(ITagContainer diContainer) iconRenderer.Material.MainSampler.Sampler = iconFont.Sampler; HandleResize(); } + public void SyncWithScene() + { + foreach (var trigger in triggers) + trigger.SyncWithScene(); + } protected override void DisposeManaged() { @@ -121,6 +131,53 @@ protected override void DisposeManaged() trigger.Dispose(); } + public void DuplicateCurrentTrigger() + { + var currentTrigger = FindCurrentTrigger(); + if (currentTrigger == null || editor.scene == null) + return; + + SyncWithScene(); + + var copy = currentTrigger.SceneTrigger.Clone(); + copy.idx = GetNextAvailableTriggerID(); + editor.scene.triggers = [.. editor.scene.triggers, copy]; + HandleLoadScene(); + editor.Selected = triggers.Last(); + } + public void DeleteCurrentTrigger() + { + var currentTrigger = FindCurrentTrigger(); + if (currentTrigger == null || editor.scene == null) + return; + + SyncWithScene(); + + editor.scene.triggers = editor.scene.triggers.Where( + trigger => trigger.idx != currentTrigger.SceneTrigger.idx + ).ToArray(); + + HandleLoadScene(); + editor.Selected = null; + } + private uint GetNextAvailableTriggerID() + { + uint result = 1; + foreach (var trigger in triggers) + { + result = Math.Max(result, trigger.SceneTrigger.idx); + } + return result + 1; + } + private Trigger? FindCurrentTrigger() + { + foreach (var trigger in triggers) + { + if (trigger == editor.Selected) + return trigger; + } + return null; + } private void HandleLoadScene() { foreach (var oldTrigger in triggers) diff --git a/zzre/tools/sceneeditor/SceneEditor.cs b/zzre/tools/sceneeditor/SceneEditor.cs index 738f75fc..567ecb62 100644 --- a/zzre/tools/sceneeditor/SceneEditor.cs +++ b/zzre/tools/sceneeditor/SceneEditor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Numerics; using Veldrid; @@ -8,6 +8,8 @@ using zzre.materials; using zzre.rendering; +using KeyCode = Silk.NET.SDL.KeyCode; + namespace zzre.tools; public partial class SceneEditor : ListDisposable, IDocumentEditor @@ -23,6 +25,9 @@ public partial class SceneEditor : ListDisposable, IDocumentEditor private readonly AssetLocalRegistry assetRegistry; private readonly DefaultEcs.World ecsWorld; + private readonly TriggerComponent triggerComponent; + private readonly FOModelComponent foModelComponent; + private event Action OnLoadScene = () => { }; private readonly ITagContainer localDiContainer; @@ -31,6 +36,8 @@ public partial class SceneEditor : ListDisposable, IDocumentEditor public IResource? CurrentResource { get; private set; } public Window Window { get; } + private bool ControlIsPressed; + public SceneEditor(ITagContainer diContainer) { resourcePool = diContainer.GetTag(); @@ -46,6 +53,9 @@ public SceneEditor(ITagContainer diContainer) AddDisposable(locationBuffer); var menuBar = new MenuBarWindowTag(Window); menuBar.AddButton("Open", HandleMenuOpen); + menuBar.AddButton("Save", SaveScene); + menuBar.AddButton("Duplicate Selection", DuplicateCurrentSelection); + menuBar.AddButton("Delete Selection", DeleteCurrentSelection); openFileModal = new OpenFileModal(diContainer) { Filter = "*.scn", @@ -81,13 +91,16 @@ public SceneEditor(ITagContainer diContainer) new DatasetComponent(localDiContainer); new WorldComponent(localDiContainer); new ModelComponent(localDiContainer); - new FOModelComponent(localDiContainer); - new TriggerComponent(localDiContainer); new LightComponent(localDiContainer); new EffectComponent(localDiContainer); new Sample3DComponent(localDiContainer); new WaypointComponent(localDiContainer); new SelectionComponent(localDiContainer); + foModelComponent = new FOModelComponent(localDiContainer); + triggerComponent = new TriggerComponent(localDiContainer); + Window.OnKeyUp += HandleKeyUp; + Window.OnKeyDown += HandleKeyDown; + Window.OnContent += HandleOnContent; diContainer.GetTag().AddEditor(this); } @@ -98,6 +111,43 @@ protected override void DisposeManaged() assetRegistry.Dispose(); } + private void HandleKeyDown(KeyCode key) + { + if (key == KeyCode.KLctrl) + ControlIsPressed = true; + } + private void HandleKeyUp(KeyCode key) + { + if (key == KeyCode.KLctrl) + ControlIsPressed = false; + else if (ControlIsPressed) + { + if (key == KeyCode.KD) + DuplicateCurrentSelection(); + else if (key == KeyCode.KS) + SaveScene(); + else if (key == KeyCode.KX) + DeleteCurrentSelection(); + } + } + private void HandleOnContent() + { + if (Window.IsFocused == false) + { + ControlIsPressed = false; + } + } + private void DuplicateCurrentSelection() + { + triggerComponent.DuplicateCurrentTrigger(); + foModelComponent.DuplicateCurrentFoModel(); + + } + private void DeleteCurrentSelection() + { + triggerComponent.DeleteCurrentTrigger(); + foModelComponent.DeleteCurrentFoModel(); + } public void Load(string pathText) { var resource = resourcePool.FindFile(pathText) ?? throw new FileNotFoundException($"Could not find world at {pathText}"); @@ -124,6 +174,7 @@ private void LoadSceneNow(IResource resource) ecsWorld.Publish(new game.messages.SceneLoaded(scene, Savegame: null!)); OnLoadScene(); assetRegistry.ApplyAssets(); + editor.ResetColumnWidth(); } private void HandleResize() => camera.Aspect = fbArea.Ratio; @@ -133,4 +184,16 @@ private void HandleMenuOpen() openFileModal.InitialSelectedResource = CurrentResource; openFileModal.Modal.Open(); } + private void SaveScene() + { + if (CurrentResource == null || scene == null) + return; + triggerComponent.SyncWithScene(); + foModelComponent.SyncWithScene(); + var path = Path.Combine(Environment.CurrentDirectory, "..", CurrentResource.Path.ToString()); + + var stream = new FileStream(path, FileMode.Create); + scene.Write(stream); + } + }