diff --git a/AssetEditor/AssetEditor.csproj b/AssetEditor/AssetEditor.csproj index 5771acb38..a594d0e65 100644 --- a/AssetEditor/AssetEditor.csproj +++ b/AssetEditor/AssetEditor.csproj @@ -27,6 +27,7 @@ + diff --git a/AssetEditor/Services/DependencyInjectionConfig.cs b/AssetEditor/Services/DependencyInjectionConfig.cs index fd9bd917b..14b8fdede 100644 --- a/AssetEditor/Services/DependencyInjectionConfig.cs +++ b/AssetEditor/Services/DependencyInjectionConfig.cs @@ -35,6 +35,7 @@ public DependencyInjectionConfig(bool loadResources = true) new Editors.AnimatioReTarget.DependencyInjectionContainer(), new Editors.Twui.DependencyInjectionContainer(), new Editors.Ipc.DependencyInjectionContainer(), + new Editors.BmdEditor.DependencyInjectionContainer(), // Host application new DependencyInjectionContainer(), diff --git a/AssetEditor/ViewModels/MenuBarViewModel.cs b/AssetEditor/ViewModels/MenuBarViewModel.cs index 824aeea9a..48327e25c 100644 --- a/AssetEditor/ViewModels/MenuBarViewModel.cs +++ b/AssetEditor/ViewModels/MenuBarViewModel.cs @@ -9,6 +9,7 @@ using Editors.AnimationFragmentEditor.AnimationPack.Commands; using Editors.Reports.Animation; using Editors.Reports.Audio; +using Editors.Reports.Bmd; using Editors.Reports.DeepSearch; using Editors.Reports.Files; using Editors.Reports.Geometry; @@ -88,6 +89,7 @@ [RelayCommand] private void CreateNewPackFile() [RelayCommand] private void SaveActivePack() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void OpenWh2AnimpackUpdater() => new AnimPackUpdaterService(_packfileService).Process(); [RelayCommand] private void GenerateRmv2Report() => _uiCommandFactory.Create().Execute(); + [RelayCommand] private void GenerateBmdReport() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void GenerateMetaDataReport() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void GenerateFileListReport() => _uiCommandFactory.Create().Execute(); [RelayCommand] private void GenerateMetaDataJsonsReport() => _uiCommandFactory.Create().Execute(); diff --git a/AssetEditor/Views/MenuBarView.xaml b/AssetEditor/Views/MenuBarView.xaml index 55ca66064..0cce5aa0c 100644 --- a/AssetEditor/Views/MenuBarView.xaml +++ b/AssetEditor/Views/MenuBarView.xaml @@ -100,6 +100,7 @@ + diff --git a/Editors/BmdEditor/DependencyInjectionContainer.cs b/Editors/BmdEditor/DependencyInjectionContainer.cs new file mode 100644 index 000000000..802dcd302 --- /dev/null +++ b/Editors/BmdEditor/DependencyInjectionContainer.cs @@ -0,0 +1,51 @@ +using Editors.BmdEditor.ViewModels; +using Editors.BmdEditor.Views; +using Editors.BmdEditor.Services; +using Microsoft.Extensions.DependencyInjection; +using Shared.Core.DependencyInjection; +using Shared.Core.DevConfig; +using Shared.Core.ToolCreation; +using Shared.Core.ErrorHandling; +using Shared.Core.Services; +using Shared.Core.Events; +using GameWorld.Core.Services; +using GameWorld.Core.Components.Rendering; +using GameWorld.Core.Rendering.Materials; +using GameWorld.Core.Rendering.Geometry; +using GameWorld.Core.WpfWindow; +using GameWorld.Core.Components.Selection; + +namespace Editors.BmdEditor +{ + public class DependencyInjectionContainer : DependencyContainer + { + public override void Register(IServiceCollection serviceCollection) + { + // Views + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + + // ViewModels + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + + // Services + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + + RegisterAllAsInterface(serviceCollection, ServiceLifetime.Transient); + } + + public override void RegisterTools(IEditorDatabase editorDatabase) + { + EditorInfoBuilder + .Create(EditorEnums.BMD_Editor) + .AddExtention(".bmd", EditorPriorites.Default) + //.AddExtention(".bin", EditorPriorites.Default) // TODO: Re-enable when BMD parser is complete + .Build(editorDatabase); + } + } +} diff --git a/Editors/BmdEditor/Editors.BmdEditor.csproj b/Editors/BmdEditor/Editors.BmdEditor.csproj new file mode 100644 index 000000000..b9de587db --- /dev/null +++ b/Editors/BmdEditor/Editors.BmdEditor.csproj @@ -0,0 +1,15 @@ + + + net10.0-windows + true + enable + + + + + + + + + + diff --git a/Editors/BmdEditor/Services/BmdElementLoader.cs b/Editors/BmdEditor/Services/BmdElementLoader.cs new file mode 100644 index 000000000..cce5ede9e --- /dev/null +++ b/Editors/BmdEditor/Services/BmdElementLoader.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.GameFormats.Bmd; +using Serilog; +using Editors.BmdEditor.ViewModels; + +namespace Editors.BmdEditor.Services +{ + /// + /// Service for recursively loading BMD elements and their child elements + /// + public class BmdElementLoader + { + private readonly IPackFileService _packFileService; + private readonly ILogger _logger = Serilog.Log.ForContext(); + private readonly BmdSceneCreator _bmdSceneCreator; + + public BmdElementLoader(IPackFileService packFileService, BmdSceneCreator bmdSceneCreator) + { + _packFileService = packFileService; + _bmdSceneCreator = bmdSceneCreator; + } + + /// + /// Loads all elements from a BMD file into the provided collections + /// + public void LoadElements(BmdFile bmdFile, + ObservableCollection allElements, + ObservableCollection bmdInfos, + ObservableCollection battlefieldBuildings, + ObservableCollection battlefieldBuildingFars, + ObservableCollection captureLocations, + ObservableCollection efLines, + ObservableCollection goOutlines, + ObservableCollection nonTerrainOutlines, + ObservableCollection buildingProjectileEmitters, + ObservableCollection zonesTemplates, + ObservableCollection props, + ObservableCollection vfxInfos, + ObservableCollection pointLights, + ObservableCollection spotLights, + ObservableCollection sounds, + ObservableCollection polyMeshes, + ObservableCollection lightProbes, + ObservableCollection terrainHoles, + ObservableCollection playableAreas, + ObservableCollection cscInfos, + ObservableCollection deployments, + bool loadChildBmds = true) + { + if (bmdFile == null) return; + + // Load all element types using helper methods + LoadBmdInfos(bmdFile, bmdInfos, allElements, loadChildBmds); + LoadBattlefieldBuildings(bmdFile, battlefieldBuildings, allElements); + LoadBattlefieldBuildingFars(bmdFile, battlefieldBuildingFars, allElements); + LoadCaptureLocations(bmdFile, captureLocations, allElements); + LoadEFLines(bmdFile, efLines, allElements); + LoadGoOutlines(bmdFile, goOutlines, allElements); + LoadNonTerrainOutlines(bmdFile, nonTerrainOutlines, allElements); + LoadBuildingProjectileEmitters(bmdFile, buildingProjectileEmitters, allElements); + LoadZonesTemplates(bmdFile, zonesTemplates, allElements); + LoadProps(bmdFile, props, allElements); + LoadVfxInfos(bmdFile, vfxInfos, allElements); + LoadPointLights(bmdFile, pointLights, allElements); + LoadSpotLights(bmdFile, spotLights, allElements); + LoadSounds(bmdFile, sounds, allElements); + LoadPolyMeshes(bmdFile, polyMeshes, allElements); + LoadLightProbes(bmdFile, lightProbes, allElements); + LoadTerrainHoles(bmdFile, terrainHoles, allElements); + LoadPlayableAreas(bmdFile, playableAreas, allElements); + LoadCscInfos(bmdFile, cscInfos, allElements); + LoadDeployments(bmdFile, deployments, allElements); + + // After all view models are created, load the 3D scene with direct references + _bmdSceneCreator.LoadSceneContent(bmdFile, allElements); + } + + /// + /// Loads child elements for a BmdInfoViewModel by recursively loading the referenced BMD file + /// + public void LoadChildElements(BmdInfoViewModel parentViewModel, BmdFile referencedBmd) + { + if (parentViewModel == null || referencedBmd == null) return; + + parentViewModel.ChildElements.Clear(); + parentViewModel.HasChildren = false; + + _logger.Information($"Loading child elements for BMD: {parentViewModel.Bmd.BmdString}"); + _logger.Information($"Referenced BMD has {referencedBmd.PropInfos.Count} props, {referencedBmd.VfxInfos.Count} VFX, {referencedBmd.PointLights.Count} lights"); + + // Create temporary collections for child elements + var childAllElements = new ObservableCollection(); + var childBmdInfos = new ObservableCollection(); + var childBattlefieldBuildings = new ObservableCollection(); + var childBattlefieldBuildingFars = new ObservableCollection(); + var childCaptureLocations = new ObservableCollection(); + var childEfLines = new ObservableCollection(); + var childGoOutlines = new ObservableCollection(); + var childNonTerrainOutlines = new ObservableCollection(); + var childBuildingProjectileEmitters = new ObservableCollection(); + var childZonesTemplates = new ObservableCollection(); + var childProps = new ObservableCollection(); + var childVfxInfos = new ObservableCollection(); + var childPointLights = new ObservableCollection(); + var childSpotLights = new ObservableCollection(); + var childSounds = new ObservableCollection(); + var childPolyMeshes = new ObservableCollection(); + var childLightProbes = new ObservableCollection(); + var childTerrainHoles = new ObservableCollection(); + var childPlayableAreas = new ObservableCollection(); + var childCscInfos = new ObservableCollection(); + var childDeployments = new ObservableCollection(); + + // Load all elements from the referenced BMD (but don't load nested BMDs to avoid infinite recursion) + LoadElements(referencedBmd, + childAllElements, childBmdInfos, childBattlefieldBuildings, childBattlefieldBuildingFars, + childCaptureLocations, childEfLines, childGoOutlines, childNonTerrainOutlines, + childBuildingProjectileEmitters, childZonesTemplates, childProps, childVfxInfos, + childPointLights, childSpotLights, childSounds, childPolyMeshes, childLightProbes, + childTerrainHoles, childPlayableAreas, childCscInfos, childDeployments, + loadChildBmds: false); + + // Add all loaded elements to the parent's Children collection + foreach (var element in childAllElements) + { + parentViewModel.ChildElements.Add(element); + parentViewModel.HasChildren = true; + } + + _logger.Information($"Successfully loaded {parentViewModel.ChildElements.Count} child elements for BMD: {parentViewModel.Bmd.BmdString}"); + } + + #region Helper Methods for Loading Specific Element Types + + private void LoadBmdInfos(BmdFile bmdFile, ObservableCollection bmdInfos, + ObservableCollection allElements, bool loadChildBmds) + { + for (int i = 0; i < bmdFile.BmdInfos.Count; i++) + { + var bmd = bmdFile.BmdInfos[i]; + _logger.Information($"Processing BMD reference: {bmd.BmdString}"); + var vm = new BmdInfoViewModel(bmd); + + // Load child elements if requested + if (loadChildBmds) + { + try + { + var referencedBmdFile = _packFileService.FindFile(bmd.BmdString); + if (referencedBmdFile != null) + { + _logger.Information($"Found referenced BMD file: {bmd.BmdString}"); + var referencedBmdData = referencedBmdFile.DataSource.ReadData(); + var referencedBmd = BmdParser.Parse(referencedBmdData); + LoadChildElements(vm, referencedBmd); + _logger.Information($"Successfully loaded {vm.ChildElements.Count} child elements for BMD: {bmd.BmdString}"); + } + else + { + _logger.Warning($"Referenced BMD file not found: {bmd.BmdString}"); + } + } + catch (Exception ex) + { + _logger.Warning($"Failed to load referenced BMD '{bmd.BmdString}' for UI: {ex.Message}"); + } + } + + bmdInfos.Add(vm); + allElements.Add(vm); + } + } + + private void LoadBattlefieldBuildings(BmdFile bmdFile, ObservableCollection battlefieldBuildings, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.BattlefieldBuildings.Count; i++) + { + var building = bmdFile.BattlefieldBuildings[i]; + var vm = new BattlefieldBuildingViewModel(building); + battlefieldBuildings.Add(vm); + allElements.Add(vm); + } + } + + private void LoadBattlefieldBuildingFars(BmdFile bmdFile, ObservableCollection battlefieldBuildingFars, + ObservableCollection allElements) + { + foreach (var buildingFar in bmdFile.BattlefieldBuildingFars) + { + var vm = new BattlefieldBuildingFarViewModel(buildingFar); + battlefieldBuildingFars.Add(vm); + allElements.Add(vm); + } + } + + private void LoadCaptureLocations(BmdFile bmdFile, ObservableCollection captureLocations, + ObservableCollection allElements) + { + foreach (var captureLocation in bmdFile.CaptureLocations) + { + var vm = new CaptureLocationViewModel(captureLocation); + captureLocations.Add(vm); + allElements.Add(vm); + } + } + + private void LoadEFLines(BmdFile bmdFile, ObservableCollection efLines, + ObservableCollection allElements) + { + foreach (var efLine in bmdFile.EFLines) + { + var vm = new EFLineViewModel(efLine); + efLines.Add(vm); + allElements.Add(vm); + } + } + + private void LoadGoOutlines(BmdFile bmdFile, ObservableCollection goOutlines, + ObservableCollection allElements) + { + foreach (var goOutline in bmdFile.GoOutlines) + { + var vm = new GoOutlineViewModel(goOutline); + goOutlines.Add(vm); + allElements.Add(vm); + } + } + + private void LoadNonTerrainOutlines(BmdFile bmdFile, ObservableCollection nonTerrainOutlines, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.NonTerrainOutlines.Count; i++) + { + var nonTerrainOutline = bmdFile.NonTerrainOutlines[i]; + var vm = new NonTerrainOutlineViewModel(nonTerrainOutline); + nonTerrainOutlines.Add(vm); + allElements.Add(vm); + } + } + + private void LoadBuildingProjectileEmitters(BmdFile bmdFile, ObservableCollection buildingProjectileEmitters, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.BuildingProjectileEmitters.Count; i++) + { + var buildingProjectileEmitter = bmdFile.BuildingProjectileEmitters[i]; + var vm = new BuildingProjectileEmitterViewModel(buildingProjectileEmitter); + buildingProjectileEmitters.Add(vm); + allElements.Add(vm); + } + } + + private void LoadZonesTemplates(BmdFile bmdFile, ObservableCollection zonesTemplates, + ObservableCollection allElements) + { + foreach (var zonesTemplate in bmdFile.ZonesTemplates) + { + var vm = new ZonesTemplateViewModel(zonesTemplate); + zonesTemplates.Add(vm); + allElements.Add(vm); + } + } + + private void LoadProps(BmdFile bmdFile, ObservableCollection props, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.PropInfos.Count; i++) + { + var propInfo = bmdFile.PropInfos[i]; + var vm = new PropInfoViewModel(propInfo, propInfo.Rmv2Path); + props.Add(vm); + allElements.Add(vm); + } + } + + private void LoadVfxInfos(BmdFile bmdFile, ObservableCollection vfxInfos, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.VfxInfos.Count; i++) + { + var vfx = bmdFile.VfxInfos[i]; + var vm = new VfxInfoViewModel(vfx); + vfxInfos.Add(vm); + allElements.Add(vm); + } + } + + private void LoadPointLights(BmdFile bmdFile, ObservableCollection pointLights, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.PointLights.Count; i++) + { + var light = bmdFile.PointLights[i]; + var vm = new PointLightInfoViewModel(light); + pointLights.Add(vm); + allElements.Add(vm); + } + } + + private void LoadSpotLights(BmdFile bmdFile, ObservableCollection spotLights, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.SpotLights.Count; i++) + { + var light = bmdFile.SpotLights[i]; + var vm = new SpotLightInfoViewModel(light); + spotLights.Add(vm); + allElements.Add(vm); + } + } + + private void LoadSounds(BmdFile bmdFile, ObservableCollection sounds, + ObservableCollection allElements) + { + foreach (var sound in bmdFile.Sounds) + { + var vm = new SoundInfoViewModel(sound); + sounds.Add(vm); + allElements.Add(vm); + } + } + + private void LoadPolyMeshes(BmdFile bmdFile, ObservableCollection polyMeshes, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.PolyMeshes.Count; i++) + { + var mesh = bmdFile.PolyMeshes[i]; + var vm = new PolyMeshInfoViewModel(mesh); + polyMeshes.Add(vm); + allElements.Add(vm); + } + } + + private void LoadLightProbes(BmdFile bmdFile, ObservableCollection lightProbes, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.LightProbes.Count; i++) + { + var probe = bmdFile.LightProbes[i]; + var vm = new LightProbeInfoViewModel(probe); + lightProbes.Add(vm); + allElements.Add(vm); + } + } + + private void LoadTerrainHoles(BmdFile bmdFile, ObservableCollection terrainHoles, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.TerrainHoles.Count; i++) + { + var hole = bmdFile.TerrainHoles[i]; + var vm = new TerrainHoleInfoViewModel(hole); + terrainHoles.Add(vm); + allElements.Add(vm); + } + } + + private void LoadPlayableAreas(BmdFile bmdFile, ObservableCollection playableAreas, + ObservableCollection allElements) + { + if (bmdFile.PlayableArea != null) + { + var vm = new PlayableAreaViewModel(bmdFile.PlayableArea); + playableAreas.Add(vm); + allElements.Add(vm); + } + } + + private void LoadCscInfos(BmdFile bmdFile, ObservableCollection cscInfos, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.CscInfos.Count; i++) + { + var csc = bmdFile.CscInfos[i]; + var vm = new CscInfoViewModel(csc); + cscInfos.Add(vm); + allElements.Add(vm); + } + } + + private void LoadDeployments(BmdFile bmdFile, ObservableCollection deployments, + ObservableCollection allElements) + { + for (int i = 0; i < bmdFile.Deployments.Count; i++) + { + var deployment = bmdFile.Deployments[i]; + var vm = new DeploymentViewModel(deployment); + deployments.Add(vm); + allElements.Add(vm); + } + } + + #endregion + } +} diff --git a/Editors/BmdEditor/Services/BmdPlaceholderNodes.cs b/Editors/BmdEditor/Services/BmdPlaceholderNodes.cs new file mode 100644 index 000000000..fdca8e42f --- /dev/null +++ b/Editors/BmdEditor/Services/BmdPlaceholderNodes.cs @@ -0,0 +1,773 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Shared.GameFormats.RigidModel; +using Shared.GameFormats.RigidModel.Transforms; +using GameWorld.Core.SceneNodes; + +namespace Editors.BmdEditor.Services +{ + // Custom node class for VFX placeholders that renders a visible cube + public class VfxPlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Purple; + public Color SelectedNodeColour { get; set; } = Color.Magenta; + public float Scale { get; set; } = 0.5f; + + public VfxPlaceholderNode(string name = "VFX_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = Matrix.CreateScale(Scale) * ModelMatrix * parentWorld; + + // Render a small cube as the placeholder + renderEngine.AddRenderLines(GameWorld.Core.Rendering.LineHelper.CreateCube(worldTransform, drawColour)); + } + } + + public override ISceneNode CreateCopyInstance() => new VfxPlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as VfxPlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Scale = Scale; + base.CopyInto(target); + } + } + + // Custom node class for CSC placeholders that renders a visible cube + public class CscPlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Orange; + public Color SelectedNodeColour { get; set; } = Color.DarkOrange; + public float Scale { get; set; } = 0.4f; + + public CscPlaceholderNode(string name = "CSC_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = Matrix.CreateScale(Scale) * ModelMatrix * parentWorld; + + // Render a small cube as the placeholder + renderEngine.AddRenderLines(GameWorld.Core.Rendering.LineHelper.CreateCube(worldTransform, drawColour)); + } + } + + public override ISceneNode CreateCopyInstance() => new CscPlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as CscPlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Scale = Scale; + base.CopyInto(target); + } + } + + // Custom node class for Light Probe placeholders that renders two spheres (inner and outer) + public class LightProbePlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Cyan; + public Color SelectedNodeColour { get; set; } = Color.DarkCyan; + public float OuterRadius { get; set; } = 1.0f; + public float InnerRadius { get; set; } = 0.5f; + + public LightProbePlaceholderNode(string name = "LightProbe_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + // Render outer sphere + var outerTransform = Matrix.CreateScale(OuterRadius) * ModelMatrix * parentWorld; + var outerSphereLines = PointLightSphereNode.CreateWireframeSphere(outerTransform, NodeColour, 16); + renderEngine.AddRenderLines(outerSphereLines); + + // Render inner sphere + var innerTransform = Matrix.CreateScale(InnerRadius) * ModelMatrix * parentWorld; + var innerSphereLines = PointLightSphereNode.CreateWireframeSphere(innerTransform, SelectedNodeColour, 16); + renderEngine.AddRenderLines(innerSphereLines); + } + } + + public override ISceneNode CreateCopyInstance() => new LightProbePlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as LightProbePlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.OuterRadius = OuterRadius; + typedTarget.InnerRadius = InnerRadius; + base.CopyInto(target); + } + } + + // Custom node class for Building Projectile Emitter placeholders that renders a visible cube + public class BuildingProjectileEmitterPlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Red; + public Color SelectedNodeColour { get; set; } = Color.DarkRed; + public float Scale { get; set; } = 0.35f; + + public BuildingProjectileEmitterPlaceholderNode(string name = "BuildingProjectileEmitter_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = Matrix.CreateScale(Scale) * ModelMatrix * parentWorld; + + // Render a small cube as the placeholder + renderEngine.AddRenderLines(GameWorld.Core.Rendering.LineHelper.CreateCube(worldTransform, drawColour)); + } + } + + public override ISceneNode CreateCopyInstance() => new BuildingProjectileEmitterPlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as BuildingProjectileEmitterPlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Scale = Scale; + base.CopyInto(target); + } + } + + // Custom node class for Battlefield Building placeholders that renders a visible cube + public class BattlefieldBuildingPlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Brown; + public Color SelectedNodeColour { get; set; } = Color.SaddleBrown; + public float Scale { get; set; } = 0.6f; + + public BattlefieldBuildingPlaceholderNode(string name = "BattlefieldBuilding_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = Matrix.CreateScale(Scale) * ModelMatrix * parentWorld; + + // Render a cube as the placeholder + renderEngine.AddRenderLines(GameWorld.Core.Rendering.LineHelper.CreateCube(worldTransform, drawColour)); + } + } + + public override ISceneNode CreateCopyInstance() => new BattlefieldBuildingPlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as BattlefieldBuildingPlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Scale = Scale; + base.CopyInto(target); + } + } + + // Custom node class for NonTerrainOutline placeholders that renders connected vertices as lines + public class NonTerrainOutlineNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Cyan; + public Color SelectedNodeColour { get; set; } = Color.DarkCyan; + public List VertexList { get; set; } = new(); + + public NonTerrainOutlineNode(string name = "NonTerrainOutline") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible && VertexList.Count >= 2) + { + var drawColour = NodeColour; + var worldTransform = ModelMatrix * parentWorld; + + // Create lines connecting all vertices in order, including closing the loop + var lineVertices = new List(); + + for (int i = 0; i < VertexList.Count; i++) + { + var currentVertex = VertexList[i]; + var nextVertex = VertexList[(i + 1) % VertexList.Count]; // Wrap around to connect last to first + + var startPos = new Vector3(currentVertex.X, 0, currentVertex.Y); + var endPos = new Vector3(nextVertex.X, 0, nextVertex.Y); + + var worldStart = Vector3.Transform(startPos, worldTransform); + var worldEnd = Vector3.Transform(endPos, worldTransform); + + // Add two vertices for each line segment + lineVertices.Add(new VertexPositionColor(worldStart, drawColour)); + lineVertices.Add(new VertexPositionColor(worldEnd, drawColour)); + } + + if (lineVertices.Count > 0) + { + renderEngine.AddRenderLines(lineVertices.ToArray()); + } + } + } + + public override ISceneNode CreateCopyInstance() => new NonTerrainOutlineNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as NonTerrainOutlineNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.VertexList = new List(VertexList); + base.CopyInto(target); + } + } + + // Custom node class for Boundary placeholders that renders connected vertices as lines + public class BoundaryNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Magenta; + public Color SelectedNodeColour { get; set; } = Color.Purple; + public List PointList { get; set; } = new(); + + public BoundaryNode(string name = "Boundary") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible && PointList.Count >= 2) + { + var drawColour = NodeColour; + var worldTransform = ModelMatrix * parentWorld; + + // Create lines connecting all points in order, including closing the loop + var lineVertices = new List(); + + for (int i = 0; i < PointList.Count; i++) + { + var currentPoint = PointList[i]; + var nextPoint = PointList[(i + 1) % PointList.Count]; // Wrap around to connect last to first + + var startPos = new Vector3(currentPoint.X, 0, currentPoint.Y); + var endPos = new Vector3(nextPoint.X, 0, nextPoint.Y); + + var worldStart = Vector3.Transform(startPos, worldTransform); + var worldEnd = Vector3.Transform(endPos, worldTransform); + + // Add two vertices for each line segment + lineVertices.Add(new VertexPositionColor(worldStart, drawColour)); + lineVertices.Add(new VertexPositionColor(worldEnd, drawColour)); + } + + if (lineVertices.Count > 0) + { + renderEngine.AddRenderLines(lineVertices.ToArray()); + } + } + } + + public override ISceneNode CreateCopyInstance() => new BoundaryNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as BoundaryNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.PointList = new List(PointList); + base.CopyInto(target); + } + } + + // Custom node class for Point Light spheres that renders a sphere with radius + public class PointLightSphereNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Yellow; + public Color SelectedNodeColour { get; set; } = Color.Orange; + public float Radius { get; set; } = 1.0f; + + public PointLightSphereNode(string name = "PointLight_Sphere") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = Matrix.CreateScale(Radius) * ModelMatrix * parentWorld; + + // Render a wireframe sphere as the point light indicator + var sphereLines = CreateWireframeSphere(worldTransform, drawColour, 16); + renderEngine.AddRenderLines(sphereLines); + } + } + + public override ISceneNode CreateCopyInstance() => new PointLightSphereNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as PointLightSphereNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Radius = Radius; + base.CopyInto(target); + } + + public static VertexPositionColor[] CreateWireframeSphere(Matrix transform, Color color, int segments) + { + var lines = new List(); + + // Create latitude lines (horizontal circles) + for (int lat = 0; lat <= segments / 2; lat++) + { + var theta = MathF.PI * lat / (segments / 2); + var sinTheta = MathF.Sin(theta); + var cosTheta = MathF.Cos(theta); + + Vector3? prevPoint = null; + + for (int lon = 0; lon <= segments; lon++) // Use <= to close the circle + { + var phi = 2 * MathF.PI * lon / segments; + var sinPhi = MathF.Sin(phi); + var cosPhi = MathF.Cos(phi); + + var x = sinTheta * cosPhi; + var y = cosTheta; + var z = sinTheta * sinPhi; + + var point = new Vector3(x, y, z); + point = Vector3.Transform(point, transform); + + if (prevPoint.HasValue) + { + lines.Add(new VertexPositionColor(prevPoint.Value, color)); + lines.Add(new VertexPositionColor(point, color)); + } + + prevPoint = point; + } + } + + // Create longitude lines (vertical lines from pole to pole) + for (int lon = 0; lon < segments; lon++) + { + var phi = 2 * MathF.PI * lon / segments; + var sinPhi = MathF.Sin(phi); + var cosPhi = MathF.Cos(phi); + + Vector3? prevPoint = null; + + for (int lat = 0; lat <= segments / 2; lat++) + { + var theta = MathF.PI * lat / (segments / 2); + var sinTheta = MathF.Sin(theta); + var cosTheta = MathF.Cos(theta); + + var x = sinTheta * cosPhi; + var y = cosTheta; + var z = sinTheta * sinPhi; + + var point = new Vector3(x, y, z); + point = Vector3.Transform(point, transform); + + if (prevPoint.HasValue) + { + lines.Add(new VertexPositionColor(prevPoint.Value, color)); + lines.Add(new VertexPositionColor(point, color)); + } + + prevPoint = point; + } + } + + return lines.ToArray(); + } + } + + // Custom node class for Spot Light cones that renders a cone with direction + public class SpotLightConeNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.LightBlue; + public Color SelectedNodeColour { get; set; } = Color.Blue; + public float Length { get; set; } = 2.0f; + public float InnerAngle { get; set; } = 0.5f; + public float OuterAngle { get; set; } = 1.0f; + public RmvVector3 Position { get; set; } = new(); + public Quaternion Quaternion { get; set; } = Quaternion.Identity; + + public SpotLightConeNode(string name = "SpotLight_Cone") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + // Create transform from position and quaternion rotation + var positionVector = new Vector3(Position.X, Position.Y, Position.Z); + var rotationMatrix = Matrix.CreateFromQuaternion(Quaternion); + var translationMatrix = Matrix.CreateTranslation(positionVector); + var localTransform = rotationMatrix * translationMatrix; + var worldTransform = localTransform * parentWorld; + + // Render outer cone (larger, more transparent) + var outerConeLines = CreateWireframeCone(worldTransform, NodeColour, Length, OuterAngle, 16); + renderEngine.AddRenderLines(outerConeLines); + + // Render inner cone (smaller, more visible) + var innerConeLines = CreateWireframeCone(worldTransform, SelectedNodeColour, Length, InnerAngle, 16); + renderEngine.AddRenderLines(innerConeLines); + } + } + + public override ISceneNode CreateCopyInstance() => new SpotLightConeNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as SpotLightConeNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Length = Length; + typedTarget.InnerAngle = InnerAngle; + typedTarget.OuterAngle = OuterAngle; + typedTarget.Position = Position; + typedTarget.Quaternion = Quaternion; + base.CopyInto(target); + } + + private static VertexPositionColor[] CreateWireframeCone(Matrix transform, Color color, float length, float angle, int segments) + { + var lines = new List(); + // Use half the angle since cone geometry uses half-angle from center axis + var radius = length * MathF.Tan(angle * 0.5f); + + // Create cone tip at origin (will be transformed to spotlight position) + var tip = new Vector3(0, 0, 0); + tip = Vector3.Transform(tip, transform); + + // Create base circle points extending along positive X axis (X as forward) + var basePoints = new Vector3[segments]; + for (int i = 0; i < segments; i++) + { + var theta = 2 * MathF.PI * i / segments; + basePoints[i] = new Vector3(length, radius * MathF.Cos(theta), radius * MathF.Sin(theta)); + basePoints[i] = Vector3.Transform(basePoints[i], transform); + } + + // Create lines from tip to base + for (int i = 0; i < segments; i++) + { + lines.Add(new VertexPositionColor(tip, color)); + lines.Add(new VertexPositionColor(basePoints[i], color)); + } + + // Create base circle lines + for (int i = 0; i < segments; i++) + { + var next = (i + 1) % segments; + lines.Add(new VertexPositionColor(basePoints[i], color)); + lines.Add(new VertexPositionColor(basePoints[next], color)); + } + + return lines.ToArray(); + } + } + + // Custom node class for Terrain Hole edges that renders triangle edges only + public class TerrainHoleEdgesNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Red; + public Color SelectedNodeColour { get; set; } = Color.DarkRed; + public RmvVector3 FirstVert { get; set; } = new(); + public RmvVector3 SecondVert { get; set; } = new(); + public RmvVector3 ThirdVert { get; set; } = new(); + + public TerrainHoleEdgesNode(string name = "TerrainHole_Edges") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = ModelMatrix * parentWorld; + + // Create triangle edges (3 lines forming a triangle, no filled faces) + var v1 = new Vector3(FirstVert.X, FirstVert.Y, FirstVert.Z); + var v2 = new Vector3(SecondVert.X, SecondVert.Y, SecondVert.Z); + var v3 = new Vector3(ThirdVert.X, ThirdVert.Y, ThirdVert.Z); + + // Transform vertices to world space + v1 = Vector3.Transform(v1, worldTransform); + v2 = Vector3.Transform(v2, worldTransform); + v3 = Vector3.Transform(v3, worldTransform); + + // Draw triangle edges + renderEngine.AddRenderLines(new[] { + new VertexPositionColor(v1, drawColour), + new VertexPositionColor(v2, drawColour), + new VertexPositionColor(v2, drawColour), + new VertexPositionColor(v3, drawColour), + new VertexPositionColor(v3, drawColour), + new VertexPositionColor(v1, drawColour) + }); + } + } + + public override ISceneNode CreateCopyInstance() => new TerrainHoleEdgesNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as TerrainHoleEdgesNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.FirstVert = FirstVert; + typedTarget.SecondVert = SecondVert; + typedTarget.ThirdVert = ThirdVert; + base.CopyInto(target); + } + } + + // Custom node class for PolyMesh triangles that renders filled triangles with materials + public class PolyMeshTrianglesNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Green; + public Color SelectedNodeColour { get; set; } = Color.DarkGreen; + public RmvVector3[] Vertices { get; set; } = Array.Empty(); + public ushort[] Triangles { get; set; } = Array.Empty(); + public string MaterialString { get; set; } = string.Empty; + + public PolyMeshTrianglesNode(string name = "PolyMesh_Triangles") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible && Vertices.Length > 0 && Triangles.Length >= 3) + { + var drawColour = NodeColour; + var worldTransform = ModelMatrix * parentWorld; + + // Convert vertices to world space + var worldVertices = new Vector3[Vertices.Length]; + for (int i = 0; i < Vertices.Length; i++) + { + var vertex = new Vector3(Vertices[i].X, Vertices[i].Y, Vertices[i].Z); + worldVertices[i] = Vector3.Transform(vertex, worldTransform); + } + + // Create triangle edges (for now, render as wireframe - filled triangles would require proper mesh rendering) + var lineVertices = new List(); + + for (int i = 0; i < Triangles.Length; i += 3) + { + if (i + 2 < Triangles.Length) + { + var idx1 = Triangles[i]; + var idx2 = Triangles[i + 1]; + var idx3 = Triangles[i + 2]; + + if (idx1 < worldVertices.Length && idx2 < worldVertices.Length && idx3 < worldVertices.Length) + { + // Add triangle edges + lineVertices.Add(new VertexPositionColor(worldVertices[idx1], drawColour)); + lineVertices.Add(new VertexPositionColor(worldVertices[idx2], drawColour)); + lineVertices.Add(new VertexPositionColor(worldVertices[idx2], drawColour)); + lineVertices.Add(new VertexPositionColor(worldVertices[idx3], drawColour)); + lineVertices.Add(new VertexPositionColor(worldVertices[idx3], drawColour)); + lineVertices.Add(new VertexPositionColor(worldVertices[idx1], drawColour)); + } + } + } + + if (lineVertices.Count > 0) + { + renderEngine.AddRenderLines(lineVertices.ToArray()); + } + } + } + + public override ISceneNode CreateCopyInstance() => new PolyMeshTrianglesNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as PolyMeshTrianglesNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Vertices = Vertices; + typedTarget.Triangles = Triangles; + typedTarget.MaterialString = MaterialString; + base.CopyInto(target); + } + } + + // Custom node class for BMD Info placeholders that represents recursive BMD file references + public class BmdInfoPlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.White; + public Color SelectedNodeColour { get; set; } = Color.Gray; + public float Scale { get; set; } = 0.8f; + public string ReferencedBmdPath { get; set; } = string.Empty; + + public BmdInfoPlaceholderNode(string name = "BMD_Info_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = Matrix.CreateScale(Scale) * ModelMatrix * parentWorld; + + // Render a larger cube as the BMD reference placeholder + renderEngine.AddRenderLines(GameWorld.Core.Rendering.LineHelper.CreateCube(worldTransform, drawColour)); + } + } + + public override ISceneNode CreateCopyInstance() => new BmdInfoPlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as BmdInfoPlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Scale = Scale; + typedTarget.ReferencedBmdPath = ReferencedBmdPath; + base.CopyInto(target); + } + } + + // Custom node class for Prop placeholders that renders a visible cube when RMV2 loading fails + public class PropPlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Red; + public Color SelectedNodeColour { get; set; } = Color.DarkRed; + public float Scale { get; set; } = 0.5f; + public string FailedModelPath { get; set; } = string.Empty; + + public PropPlaceholderNode(string name = "Prop_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible) + { + var drawColour = NodeColour; + var worldTransform = Matrix.CreateScale(Scale) * ModelMatrix * parentWorld; + + // Render a cube as the prop placeholder + renderEngine.AddRenderLines(GameWorld.Core.Rendering.LineHelper.CreateCube(worldTransform, drawColour)); + } + } + + public override ISceneNode CreateCopyInstance() => new PropPlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as PropPlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Scale = Scale; + typedTarget.FailedModelPath = FailedModelPath; + base.CopyInto(target); + } + } + + // Custom node class for Sound placeholders that renders cubes at coordinates with optional connecting lines + public class SoundPlaceholderNode : GroupNode, IDrawableItem + { + public Color NodeColour { get; set; } = Color.Lime; + public Color SelectedNodeColour { get; set; } = Color.Green; + public float Scale { get; set; } = 0.3f; + public string SoundType { get; set; } = string.Empty; + public RmvVector3[] CoordList { get; set; } = Array.Empty(); + + public SoundPlaceholderNode(string name = "Sound_Placeholder") : base(name) + { + } + + public void Render(GameWorld.Core.Components.Rendering.RenderEngineComponent renderEngine, Matrix parentWorld) + { + if (IsVisible && CoordList.Length > 0) + { + var drawColour = NodeColour; + var worldTransform = ModelMatrix * parentWorld; + + // Render cubes at all coordinates + foreach (var coord in CoordList) + { + var position = new Vector3(coord.X, coord.Y, coord.Z); + var cubeTransform = Matrix.CreateScale(Scale) * Matrix.CreateTranslation(position) * worldTransform; + renderEngine.AddRenderLines(GameWorld.Core.Rendering.LineHelper.CreateCube(cubeTransform, drawColour)); + } + + // For LINE_LIST, also draw connecting lines between vertices + if (SoundType == "SST_LINE_LIST" && CoordList.Length >= 2) + { + var lineVertices = new List(); + + for (int i = 0; i < CoordList.Length - 1; i++) + { + var currentVertex = CoordList[i]; + var nextVertex = CoordList[i + 1]; + + var startPos = new Vector3(currentVertex.X, currentVertex.Y, currentVertex.Z); + var endPos = new Vector3(nextVertex.X, nextVertex.Y, nextVertex.Z); + + var worldStart = Vector3.Transform(startPos, worldTransform); + var worldEnd = Vector3.Transform(endPos, worldTransform); + + // Add two vertices for each line segment + lineVertices.Add(new VertexPositionColor(worldStart, drawColour)); + lineVertices.Add(new VertexPositionColor(worldEnd, drawColour)); + } + + if (lineVertices.Count > 0) + { + renderEngine.AddRenderLines(lineVertices.ToArray()); + } + } + } + } + + public override ISceneNode CreateCopyInstance() => new SoundPlaceholderNode(); + + public override void CopyInto(ISceneNode target) + { + var typedTarget = target as SoundPlaceholderNode; + typedTarget.NodeColour = NodeColour; + typedTarget.SelectedNodeColour = SelectedNodeColour; + typedTarget.Scale = Scale; + typedTarget.SoundType = SoundType; + typedTarget.CoordList = CoordList; + base.CopyInto(target); + } + } +} diff --git a/Editors/BmdEditor/Services/BmdSceneCreator.cs b/Editors/BmdEditor/Services/BmdSceneCreator.cs new file mode 100644 index 000000000..969a92565 --- /dev/null +++ b/Editors/BmdEditor/Services/BmdSceneCreator.cs @@ -0,0 +1,817 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.GameFormats.Bmd; +using Shared.GameFormats.RigidModel; +using Shared.GameFormats.RigidModel.Transforms; +using Shared.GameFormats.WsModel; +using GameWorld.Core.Components; +using GameWorld.Core.SceneNodes; +using GameWorld.Core.Services; +using Serilog; +using Editors.BmdEditor.ViewModels; +using Shared.Core.Events; +using GameWorld.Core.Components.Selection; + +namespace Editors.BmdEditor.Services +{ + public class BmdSceneCreator + { + private readonly ILogger _logger = Serilog.Log.ForContext(); + private readonly IPackFileService _packFileService; + private readonly GameWorld.Core.Components.SceneManager _sceneManager; + private readonly GameWorld.Core.SceneNodes.Rmv2ModelNodeLoader _rmv2ModelNodeLoader; + private readonly ResourceLibrary _resourceLibrary; + private readonly GameWorld.Core.Services.MeshBuilderService _meshBuilderService; + + // Store direct references between view models and scene nodes + public readonly Dictionary ComponentNodes = new(); + private GameWorld.Core.SceneNodes.SceneNode? _currentHighlightedNode; + + public BmdSceneCreator( + IPackFileService packFileService, + GameWorld.Core.Components.SceneManager sceneManager, + GameWorld.Core.SceneNodes.Rmv2ModelNodeLoader rmv2ModelNodeLoader, + ResourceLibrary resourceLibrary, + GameWorld.Core.Services.MeshBuilderService meshBuilderService) + { + _packFileService = packFileService; + _sceneManager = sceneManager; + _rmv2ModelNodeLoader = rmv2ModelNodeLoader; + _resourceLibrary = resourceLibrary; + _meshBuilderService = meshBuilderService; + } + + private GroupNode? _propsGroup; + private GroupNode? _otherGroup; + + public GroupNode GetPropsGroup() => _propsGroup!; + public GroupNode GetOtherGroup() => _otherGroup!; + + public void CreateSceneFromBmd(BmdFile bmdFile, PackFile bmdPackFile) + { + _logger.Information("Creating 3D scene from BMD file: {FileName}", bmdPackFile.Name); + + try + { + // Clear existing scene + ClearScene(); + + // Create scene structure + var rootNode = _sceneManager.RootNode; + var bmdRootNode = rootNode.AddObject(new GroupNode("BMD_Scene") { IsEditable = false }); + + // Create groups for later reference + _propsGroup = bmdRootNode.AddObject(new GroupNode("Props") { IsEditable = false }); + _otherGroup = bmdRootNode.AddObject(new GroupNode("Other") { IsEditable = false }); + + _logger.Information($"BMD scene structure created successfully"); + } + catch (Exception ex) + { + _logger.Error($"Failed to create BMD scene: {ex.Message}"); + throw; + } + } + + public void LoadSceneContent(BmdFile bmdFile, ObservableCollection allElements) + { + if (_propsGroup == null || _otherGroup == null) + throw new InvalidOperationException("Scene groups not initialized. Call CreateSceneFromBmd first."); + + try + { + // Load props as RMV2 models + LoadProps(bmdFile, _propsGroup, allElements.OfType().ToList()); + + // Load other components + LoadOtherComponents(bmdFile, _otherGroup, allElements); + + _logger.Information($"BMD scene content loaded successfully"); + } + catch (Exception ex) + { + _logger.Error($"Failed to load BMD scene content: {ex.Message}"); + throw; + } + } + + private void ClearScene() + { + // Remove existing BMD scene nodes + var existingBmdNodes = _sceneManager.RootNode.Children.Where(x => x.Name == "BMD_Scene").ToList(); + foreach (var node in existingBmdNodes) + { + _sceneManager.RootNode.Children.Remove(node); + } + + // Clear node references + ComponentNodes.Clear(); + _currentHighlightedNode = null; + } + + private void LoadProps(BmdFile bmdFile, GroupNode propsGroup, List propViewModels) + { + _logger.Information($"Loading {bmdFile.PropInfos.Count} props"); + + for (int i = 0; i < bmdFile.PropInfos.Count; i++) + { + var propInfo = bmdFile.PropInfos[i]; + var propPath = propInfo.Rmv2Path; + + try + { + var node = LoadSingleProp(propPath, propInfo, propsGroup, i); // Pass instance index + var viewModel = propViewModels[i]; + ComponentNodes[viewModel] = node; + } + catch (Exception ex) + { + _logger.Error(ex, $"Failed to load prop at index {i}"); + } + } + } + + private SceneNode LoadSingleProp(string propPath, PropInfo propInfo, GroupNode propsGroup, int instanceIndex) + { + var modelFile = _packFileService.FindFile(propPath); + if (modelFile == null) + { + _logger.Warning($"Prop model file not found: {propPath}"); + return CreatePlaceholderProp(propPath, propInfo, propsGroup, instanceIndex, "File not found"); + } + + try + { + // Handle wsmodel files by getting the actual rigid_model_v2 path + var actualModelFile = modelFile; + var actualModelPath = propPath; + + if (Path.GetExtension(propPath).ToLower() == ".wsmodel") + { + var wsModel = new WsModelFile(modelFile); + if (string.IsNullOrEmpty(wsModel.GeometryPath)) + { + return CreatePlaceholderProp(propPath, propInfo, propsGroup, instanceIndex, "WsModel has no geometry path"); + } + + actualModelPath = wsModel.GeometryPath; + actualModelFile = _packFileService.FindFile(actualModelPath); + if (actualModelFile == null) + { + return CreatePlaceholderProp(propPath, propInfo, propsGroup, instanceIndex, $"Referenced file not found: {actualModelPath}"); + } + } + + // Load the RMV2 model + var modelData = actualModelFile.DataSource.ReadData(); + var rmv = ModelFactory.Create().Load(modelData); + + // Create RMV2 model nodes + var modelFullPath = _packFileService.GetFullPath(actualModelFile); + var lodNodes = _rmv2ModelNodeLoader.CreateModelNodesFromFile(rmv, modelFullPath, false, null); + + var lodNode = lodNodes.FirstOrDefault(); + if (lodNode == null) + { + return CreatePlaceholderProp(propPath, propInfo, propsGroup, instanceIndex, "No LOD nodes found"); + } + + // Apply the prop transformation directly to the RMV2 node + // This ensures all components (mesh, materials, etc.) are transformed together + lodNode.ModelMatrix = propInfo.Transform; + + // Create a simple group for the prop + var propName = $"Prop_{instanceIndex}_{Path.GetFileNameWithoutExtension(propPath)}"; + var propGroup = propsGroup.AddObject(new GroupNode(propName) { IsEditable = false }); + propGroup.AddObject(lodNode); + + _logger.Debug($"Loaded prop: {propPath} at {propInfo.Transform.Translation}"); + return propGroup; + } + catch (Exception ex) + { + _logger.Error(ex, $"Error loading prop '{propPath}': {ex.Message}"); + return CreatePlaceholderProp(propPath, propInfo, propsGroup, instanceIndex, ex.Message); + } + } + + private SceneNode CreatePlaceholderProp(string propPath, PropInfo propInfo, GroupNode propsGroup, int instanceIndex, string reason) + { + var propName = $"Prop_{instanceIndex}_{Path.GetFileNameWithoutExtension(propPath)}"; + var propGroup = propsGroup.AddObject(new GroupNode(propName) { IsEditable = false }); + + var placeholderMesh = CreatePlaceholderNode(() => new PropPlaceholderNode("Prop_Placeholder") + { + FailedModelPath = propPath + }, propInfo.Transform); + + if (placeholderMesh != null) + { + propGroup.AddObject(placeholderMesh); + } + + _logger.Debug($"Created placeholder prop: {propPath} ({reason}) at {propInfo.Transform.Translation}"); + return propGroup; + } + + + + private void LoadOtherComponents(BmdFile bmdFile, GroupNode otherGroup, ObservableCollection allElements) + { + _logger.Information("Loading other BMD components"); + + var componentLoaders = new (string Name, int Count, Action Loader)[] + { + ("Point_Lights", bmdFile.PointLights.Count, group => LoadComponents(bmdFile.PointLights, group, "PointLight", CreatePointLightPlaceholderNode, allElements.OfType().ToList())), + ("Spot_Lights", bmdFile.SpotLights.Count, group => LoadComponents(bmdFile.SpotLights, group, "SpotLight", CreateSpotLightPlaceholderNode, allElements.OfType().ToList())), + ("Sounds", bmdFile.Sounds.Count, group => LoadComponents(bmdFile.Sounds, group, "Sound", CreateSoundPlaceholderNode, allElements.OfType().ToList())), + ("VFX", bmdFile.VfxInfos.Count, group => LoadComponents(bmdFile.VfxInfos, group, "VFX", CreateVfxPlaceholderNode, allElements.OfType().ToList())), + ("CSC", bmdFile.CscInfos.Count, group => LoadComponents(bmdFile.CscInfos, group, "CSC", CreateCscPlaceholderNode, allElements.OfType().ToList())), + ("Light_Probes", bmdFile.LightProbes.Count, group => LoadComponents(bmdFile.LightProbes, group, "LightProbe", CreateLightProbePlaceholderNode, allElements.OfType().ToList())), + ("Building_Projectile_Emitters", bmdFile.BuildingProjectileEmitters.Count, group => LoadComponents(bmdFile.BuildingProjectileEmitters, group, "BuildingProjectileEmitter", CreateBuildingProjectileEmitterPlaceholderNode, allElements.OfType().ToList())), + ("Terrain_Holes", bmdFile.TerrainHoles.Count, group => LoadComponents(bmdFile.TerrainHoles, group, "TerrainHole", CreateTerrainHoleNode, allElements.OfType().ToList())), + ("PolyMeshes", bmdFile.PolyMeshes.Count, group => LoadComponents(bmdFile.PolyMeshes, group, "PolyMesh", CreatePolyMeshNode, allElements.OfType().ToList())), + ("NonTerrain_Outlines", bmdFile.NonTerrainOutlines.Count, group => LoadComponents(bmdFile.NonTerrainOutlines, group, "NonTerrainOutline", CreateNonTerrainOutlineNode, allElements.OfType().ToList())), + ("Battlefield_Buildings", bmdFile.BattlefieldBuildings.Count, group => LoadComponents(bmdFile.BattlefieldBuildings, group, "BattlefieldBuilding", CreateBattlefieldBuildingPlaceholderNode, allElements.OfType().ToList())), + ("BMD_References", bmdFile.BmdInfos.Count, group => LoadComponents(bmdFile.BmdInfos, group, "BMD", CreateBmdInfoNode, allElements.OfType().ToList())), + ("Deployments", bmdFile.Deployments.Count, group => LoadDeploymentComponents(bmdFile.Deployments, group, allElements.OfType().ToList())) + }; + + foreach (var (name, count, loader) in componentLoaders) + { + if (count > 0) + { + _logger.Information($"Creating {name} group ({count} items)"); + var group = otherGroup.AddObject(new GroupNode($"{name} ({count})") { IsEditable = false }); + loader(group); + } + } + } + + private void LoadComponents(IList components, GroupNode group, string prefix, Func nodeCreator, IList viewModels) + where TViewModel : BmdElementViewModel + { + _logger.Information($"Loading {components.Count} {prefix} components"); + + for (int i = 0; i < components.Count && i < viewModels.Count; i++) + { + try + { + var component = components[i]; + var node = nodeCreator(component, group, i); + var viewModel = viewModels[i]; + ComponentNodes[viewModel] = node; + } + catch (Exception ex) + { + _logger.Warning($"Failed to create {prefix} component at index {i}: {ex.Message}"); + } + } + } + + private SceneNode CreateVfxPlaceholderNode(VfxInfo vfxInfo, GroupNode vfxGroup, int index) + { + var vfxName = $"VFX_{index}_{Path.GetFileNameWithoutExtension(vfxInfo.VfxString)}"; + var vfxNode = vfxGroup.AddObject(new GroupNode(vfxName) { IsEditable = false }); + + var placeholderMesh = CreatePlaceholderNode(() => new VfxPlaceholderNode("VFX_Placeholder"), vfxInfo.Transform); + if (placeholderMesh != null) + { + vfxNode.AddObject(placeholderMesh); + } + + return vfxNode; + } + + private SceneNode CreateCscPlaceholderNode(CscInfo cscInfo, GroupNode cscGroup, int index) + { + var cscName = $"CSC_{index}_{Path.GetFileNameWithoutExtension(cscInfo.SceneFile)}"; + var cscNode = cscGroup.AddObject(new GroupNode(cscName) { IsEditable = false }); + + var placeholderMesh = CreatePlaceholderNode(() => new CscPlaceholderNode("CSC_Placeholder"), cscInfo.Transform); + if (placeholderMesh != null) + { + cscNode.AddObject(placeholderMesh); + } + + return cscNode; + } + + private SceneNode CreateLightProbePlaceholderNode(LightProbeInfo lightProbeInfo, GroupNode lightProbeGroup, int index) + { + var lightProbeName = $"LightProbe_{index}"; + var lightProbeNode = lightProbeGroup.AddObject(new GroupNode(lightProbeName) { IsEditable = false }); + + var positionVector = new Vector3(lightProbeInfo.Position.X, lightProbeInfo.Position.Y, lightProbeInfo.Position.Z); + var transform = Matrix.CreateTranslation(positionVector); + var placeholderMesh = CreatePlaceholderNode(() => new LightProbePlaceholderNode("LightProbe_Placeholder") + { + OuterRadius = lightProbeInfo.OuterRadius, + InnerRadius = lightProbeInfo.InnerRadius + }, transform); + if (placeholderMesh != null) + { + lightProbeNode.AddObject(placeholderMesh); + } + + return lightProbeNode; + } + + private SceneNode CreateBuildingProjectileEmitterPlaceholderNode(BuildingProjectileEmitter emitterInfo, GroupNode emitterGroup, int index) + { + var emitterName = $"BuildingProjectileEmitter_{index}"; + var emitterNode = emitterGroup.AddObject(new GroupNode(emitterName) { IsEditable = false }); + + var positionVector = new Vector3(emitterInfo.Location.X, emitterInfo.Location.Y, emitterInfo.Location.Z); + var transform = Matrix.CreateTranslation(positionVector); + var placeholderMesh = CreatePlaceholderNode(() => new BuildingProjectileEmitterPlaceholderNode("BuildingProjectileEmitter_Placeholder"), transform); + if (placeholderMesh != null) + { + emitterNode.AddObject(placeholderMesh); + } + + return emitterNode; + } + + private SceneNode? CreatePlaceholderNode(Func nodeFactory, Matrix transform) + { + try + { + var node = nodeFactory(); + node.IsEditable = false; + node.ModelMatrix = transform; + return node; + } + catch (Exception ex) + { + _logger.Error($"Failed to create placeholder node: {ex.Message}"); + return null; + } + } + + private SceneNode CreateTerrainHoleNode(TerrainHoleTriangleInfo terrainHole, GroupNode terrainHoleGroup, int index) + { + var holeName = $"TerrainHole_{index}"; + var holeNode = terrainHoleGroup.AddObject(new GroupNode(holeName) { IsEditable = false }); + + var placeholderMesh = CreateSpecializedNode(() => new TerrainHoleEdgesNode("TerrainHole_Edges") + { + FirstVert = terrainHole.FirstVert, + SecondVert = terrainHole.SecondVert, + ThirdVert = terrainHole.ThirdVert + }); + if (placeholderMesh != null) + { + holeNode.AddObject(placeholderMesh); + } + + return holeNode; + } + + private SceneNode CreatePolyMeshNode(PolyMeshInfo polyMesh, GroupNode polyMeshGroup, int index) + { + var meshName = $"PolyMesh_{index}_{Path.GetFileNameWithoutExtension(polyMesh.MaterialString)}"; + var meshNode = polyMeshGroup.AddObject(new GroupNode(meshName) { IsEditable = false }); + + var placeholderMesh = CreateSpecializedNode(() => new PolyMeshTrianglesNode("PolyMesh_Triangles") + { + Vertices = polyMesh.VertexList, + Triangles = polyMesh.TriangleList, + MaterialString = polyMesh.MaterialString + }); + if (placeholderMesh != null) + { + meshNode.AddObject(placeholderMesh); + } + + return meshNode; + } + + private SceneNode CreateBattlefieldBuildingPlaceholderNode(BattlefieldBuilding buildingInfo, GroupNode buildingsGroup, int index) + { + var buildingName = $"BattlefieldBuilding_{index}_{buildingInfo.BuildingId}"; + var buildingNode = buildingsGroup.AddObject(new GroupNode(buildingName) { IsEditable = false }); + + var placeholderMesh = CreatePlaceholderNode(() => new BattlefieldBuildingPlaceholderNode("BattlefieldBuilding_Placeholder"), buildingInfo.Transform); + if (placeholderMesh != null) + { + buildingNode.AddObject(placeholderMesh); + } + + return buildingNode; + } + + private SceneNode? CreateSpecializedNode(Func nodeFactory) + { + try + { + var node = nodeFactory(); + node.IsEditable = false; + return node; + } + catch (Exception ex) + { + _logger.Error($"Failed to create specialized node: {ex.Message}"); + return null; + } + } + + private SceneNode CreateNonTerrainOutlineNode(NonTerrainOutline outlineInfo, GroupNode nonTerrainOutlineGroup, int index) + { + var outlineName = $"NonTerrainOutline_{index}"; + var outlineNode = nonTerrainOutlineGroup.AddObject(new GroupNode(outlineName) { IsEditable = false }); + + var placeholderMesh = CreateSpecializedNode(() => new NonTerrainOutlineNode("NonTerrainOutline_Placeholder") + { + VertexList = outlineInfo.VertexList + }); + if (placeholderMesh != null) + { + outlineNode.AddObject(placeholderMesh); + } + + return outlineNode; + } + + private SceneNode CreatePointLightPlaceholderNode(PointLightInfo pointLightInfo, GroupNode pointLightGroup, int index) + { + var pointLightName = $"PointLight_{index}"; + var pointLightNode = pointLightGroup.AddObject(new GroupNode(pointLightName) { IsEditable = false }); + + var positionVector = new Vector3(pointLightInfo.Position.X, pointLightInfo.Position.Y, pointLightInfo.Position.Z); + var transform = Matrix.CreateTranslation(positionVector); + var placeholderMesh = CreateSpecializedNode(() => new PointLightSphereNode("PointLight_Sphere") + { + Radius = pointLightInfo.Radius + }); + if (placeholderMesh != null) + { + placeholderMesh.ModelMatrix = transform; + pointLightNode.AddObject(placeholderMesh); + } + + return pointLightNode; + } + + private SceneNode CreateSpotLightPlaceholderNode(SpotLightInfo spotLightInfo, GroupNode spotLightGroup, int index) + { + var spotLightName = $"SpotLight_{index}"; + var spotLightNode = spotLightGroup.AddObject(new GroupNode(spotLightName) { IsEditable = false }); + + var placeholderMesh = CreateSpecializedNode(() => new SpotLightConeNode("SpotLight_Cone") + { + Length = spotLightInfo.Length, + InnerAngle = spotLightInfo.InnerAngleRadians, + OuterAngle = spotLightInfo.OuterAngleRadians, + Position = spotLightInfo.Position, + Quaternion = new Quaternion(spotLightInfo.QuartX, spotLightInfo.QuartY, spotLightInfo.QuartZ, spotLightInfo.QuartW) + }); + if (placeholderMesh != null) + { + spotLightNode.AddObject(placeholderMesh); + } + + return spotLightNode; + } + + private SceneNode CreateSoundPlaceholderNode(SoundInfo soundInfo, GroupNode soundGroup, int index) + { + var soundName = $"Sound_{index}_{Path.GetFileNameWithoutExtension(soundInfo.SoundString)}"; + var soundNode = soundGroup.AddObject(new GroupNode(soundName) { IsEditable = false }); + + var placeholderMesh = CreateSpecializedNode(() => new SoundPlaceholderNode("Sound_Placeholder") + { + SoundType = soundInfo.TypeString, + CoordList = soundInfo.CoordList + }); + if (placeholderMesh != null) + { + soundNode.AddObject(placeholderMesh); + } + + return soundNode; + } + + + public void HighlightComponent(BmdElementViewModel component) + { + // Clear previous highlight + ClearHighlight(); + + if (ComponentNodes.TryGetValue(component, out var node)) + { + // TODO: Implement actual highlighting logic + // This might involve changing the material, adding an outline, or changing the node's properties + _currentHighlightedNode = node; + _logger.Debug($"Highlighted component: {component.ElementType} - {component.DisplayName}"); + } + else + { + _logger.Warning($"Component node not found: {component.ElementType} - {component.DisplayName}"); + } + } + + + public void ClearHighlight() + { + if (_currentHighlightedNode != null) + { + // TODO: Implement actual highlight clearing logic + _logger.Debug($"Cleared highlight from: {_currentHighlightedNode.Name}"); + _currentHighlightedNode = null; + } + } + + private SceneNode CreateBmdInfoNode(BmdInfo bmdInfo, GroupNode bmdGroup, int index) + { + var bmdName = $"BMD_{index}_{Path.GetFileNameWithoutExtension(bmdInfo.BmdString)}"; + var bmdNode = bmdGroup.AddObject(new GroupNode(bmdName) { IsEditable = false }); + + // Create placeholder node for the BMD reference + var placeholderMesh = CreatePlaceholderNode(() => new BmdInfoPlaceholderNode("BMD_Info_Placeholder") + { + ReferencedBmdPath = bmdInfo.BmdString + }, bmdInfo.Transform); + if (placeholderMesh != null) + { + bmdNode.AddObject(placeholderMesh); + } + + // Try to load the referenced BMD file recursively + try + { + LoadReferencedBmd(bmdInfo.BmdString, bmdNode, index); + } + catch (Exception ex) + { + _logger.Warning($"Failed to load referenced BMD '{bmdInfo.BmdString}' at index {index}: {ex.Message}"); + } + + return bmdNode; + } + + private void LoadReferencedBmd(string bmdPath, GroupNode parentNode, int parentIndex) + { + if (string.IsNullOrEmpty(bmdPath)) + { + _logger.Warning($"Empty BMD path for parent index {parentIndex}"); + return; + } + + // Prevent infinite recursion - this will be handled differently with direct references + + // Find the referenced BMD file + var referencedBmdFile = _packFileService.FindFile(bmdPath); + if (referencedBmdFile == null) + { + _logger.Warning($"Referenced BMD file not found: {bmdPath}"); + return; + } + + try + { + // Parse the referenced BMD file + var referencedBmdData = referencedBmdFile.DataSource.ReadData(); + var referencedBmd = BmdParser.Parse(referencedBmdData); + + // Reference will be stored by BmdElementLoader after view model is created + + // Create a group for the referenced BMD content + var referencedBmdGroup = parentNode.AddObject(new GroupNode($"Referenced_Content_{Path.GetFileNameWithoutExtension(bmdPath)}") { IsEditable = false }); + + // Load props from the referenced BMD + if (referencedBmd.PropInfos.Count > 0) + { + var propsGroup = referencedBmdGroup.AddObject(new GroupNode($"Referenced_Props ({referencedBmd.PropInfos.Count})") { IsEditable = false }); + LoadReferencedProps(referencedBmd, propsGroup, parentIndex); + } + + // Load other components from the referenced BMD (excluding BmdInfos to prevent infinite recursion) + LoadReferencedOtherComponents(referencedBmd, referencedBmdGroup, parentIndex); + + _logger.Information($"Successfully loaded referenced BMD: {bmdPath}"); + } + catch (Exception ex) + { + _logger.Error($"Failed to parse referenced BMD '{bmdPath}': {ex.Message}"); + } + } + + private void LoadReferencedProps(BmdFile referencedBmd, GroupNode propsGroup, int parentIndex) + { + _logger.Information($"Loading {referencedBmd.PropInfos.Count} props from referenced BMD"); + + for (int i = 0; i < referencedBmd.PropInfos.Count; i++) + { + try + { + var propInfo = referencedBmd.PropInfos[i]; + LoadSingleProp(propInfo.Rmv2Path, propInfo, propsGroup, parentIndex * 1000 + i); // Use unique index to avoid conflicts + } + catch (Exception ex) + { + _logger.Error($"Failed to load referenced prop at index {i}: {ex.Message}"); + } + } + } + + private void LoadReferencedOtherComponents(BmdFile referencedBmd, GroupNode otherGroup, int parentIndex) + { + _logger.Information("Loading other components from referenced BMD"); + + var componentLoaders = new (string Name, int Count, Action Loader)[] + { + ("Referenced_Point_Lights", referencedBmd.PointLights.Count, group => LoadReferencedComponents(referencedBmd.PointLights, group, "Referenced_PointLight", CreatePointLightPlaceholderNode, parentIndex)), + ("Referenced_Spot_Lights", referencedBmd.SpotLights.Count, group => LoadReferencedComponents(referencedBmd.SpotLights, group, "Referenced_SpotLight", CreateSpotLightPlaceholderNode, parentIndex)), + ("Referenced_Sounds", referencedBmd.Sounds.Count, group => LoadReferencedComponents(referencedBmd.Sounds, group, "Referenced_Sound", CreateSoundPlaceholderNode, parentIndex)), + ("Referenced_VFX", referencedBmd.VfxInfos.Count, group => LoadReferencedComponents(referencedBmd.VfxInfos, group, "Referenced_VFX", CreateVfxPlaceholderNode, parentIndex)), + ("Referenced_CSC", referencedBmd.CscInfos.Count, group => LoadReferencedComponents(referencedBmd.CscInfos, group, "Referenced_CSC", CreateCscPlaceholderNode, parentIndex)), + ("Referenced_Light_Probes", referencedBmd.LightProbes.Count, group => LoadReferencedComponents(referencedBmd.LightProbes, group, "Referenced_LightProbe", CreateLightProbePlaceholderNode, parentIndex)), + ("Referenced_Building_Projectile_Emitters", referencedBmd.BuildingProjectileEmitters.Count, group => LoadReferencedComponents(referencedBmd.BuildingProjectileEmitters, group, "Referenced_BuildingProjectileEmitter", CreateBuildingProjectileEmitterPlaceholderNode, parentIndex)), + ("Referenced_Terrain_Holes", referencedBmd.TerrainHoles.Count, group => LoadReferencedComponents(referencedBmd.TerrainHoles, group, "Referenced_TerrainHole", CreateTerrainHoleNode, parentIndex)), + ("Referenced_PolyMeshes", referencedBmd.PolyMeshes.Count, group => LoadReferencedComponents(referencedBmd.PolyMeshes, group, "Referenced_PolyMesh", CreatePolyMeshNode, parentIndex)), + ("Referenced_NonTerrain_Outlines", referencedBmd.NonTerrainOutlines.Count, group => LoadReferencedComponents(referencedBmd.NonTerrainOutlines, group, "Referenced_NonTerrainOutline", CreateNonTerrainOutlineNode, parentIndex)), + ("Referenced_Battlefield_Buildings", referencedBmd.BattlefieldBuildings.Count, group => LoadReferencedComponents(referencedBmd.BattlefieldBuildings, group, "Referenced_BattlefieldBuilding", CreateBattlefieldBuildingPlaceholderNode, parentIndex)) + // Note: We intentionally exclude BmdInfos here to prevent infinite recursion + }; + + foreach (var (name, count, loader) in componentLoaders) + { + if (count > 0) + { + _logger.Information($"Creating {name} group ({count} items)"); + var group = otherGroup.AddObject(new GroupNode($"{name} ({count})") { IsEditable = false }); + loader(group); + } + } + } + + private void LoadReferencedComponents(IList components, GroupNode group, string prefix, Func nodeCreator, int parentIndex) + { + _logger.Information($"Loading {components.Count} {prefix} components from referenced BMD"); + + for (int i = 0; i < components.Count; i++) + { + try + { + var component = components[i]; + var node = nodeCreator(component, group, parentIndex * 1000 + i); // Use unique index to avoid conflicts + // Reference will be stored by BmdElementLoader after view model is created + } + catch (Exception ex) + { + _logger.Warning($"Failed to create referenced {prefix} component at index {i}: {ex.Message}"); + } + } + } + + private void LoadDeploymentComponents(List deployments, GroupNode deploymentGroup, List deploymentViewModels) + { + _logger.Information($"Loading {deployments.Count} deployment components"); + + for (int i = 0; i < deployments.Count && i < deploymentViewModels.Count; i++) + { + try + { + var deployment = deployments[i]; + var deploymentNode = CreateDeploymentNode(deployment, deploymentGroup, i); + var viewModel = deploymentViewModels[i]; + ComponentNodes[viewModel] = deploymentNode; + + // Load deployment zones and their boundaries + LoadDeploymentZones(deployment.DeploymentZones, deploymentNode, viewModel.Children.OfType().ToList()); + } + catch (Exception ex) + { + _logger.Warning($"Failed to create deployment component at index {i}: {ex.Message}"); + } + } + } + + private void LoadDeploymentZones(List deploymentZones, SceneNode deploymentNode, List zoneViewModels) + { + for (int i = 0; i < deploymentZones.Count && i < zoneViewModels.Count; i++) + { + try + { + var deploymentZone = deploymentZones[i]; + var zoneNode = CreateDeploymentZoneNode(deploymentZone, deploymentNode, i); + var zoneViewModel = zoneViewModels[i]; + ComponentNodes[zoneViewModel] = zoneNode; + + // Load deployment zone regions and their boundaries + LoadDeploymentZoneRegions(deploymentZone.DeploymentZoneRegions, zoneNode, zoneViewModel.Children.OfType().ToList()); + } + catch (Exception ex) + { + _logger.Warning($"Failed to create deployment zone component at index {i}: {ex.Message}"); + } + } + } + + private void LoadDeploymentZoneRegions(List deploymentZoneRegions, SceneNode zoneNode, List regionViewModels) + { + for (int i = 0; i < deploymentZoneRegions.Count && i < regionViewModels.Count; i++) + { + try + { + var deploymentZoneRegion = deploymentZoneRegions[i]; + var regionNode = CreateDeploymentZoneRegionNode(deploymentZoneRegion, zoneNode, i); + var regionViewModel = regionViewModels[i]; + ComponentNodes[regionViewModel] = regionNode; + + // Load boundaries + LoadBoundaries(deploymentZoneRegion.Boundaries, regionNode, regionViewModel.Children.OfType().ToList()); + } + catch (Exception ex) + { + _logger.Warning($"Failed to create deployment zone region component at index {i}: {ex.Message}"); + } + } + } + + private void LoadBoundaries(List boundaries, SceneNode regionNode, List boundaryViewModels) + { + for (int i = 0; i < boundaries.Count && i < boundaryViewModels.Count; i++) + { + try + { + var boundary = boundaries[i]; + var boundaryNode = CreateBoundaryNode(boundary, regionNode, i); + var boundaryViewModel = boundaryViewModels[i]; + ComponentNodes[boundaryViewModel] = boundaryNode; + } + catch (Exception ex) + { + _logger.Warning($"Failed to create boundary component at index {i}: {ex.Message}"); + } + } + } + + private SceneNode CreateDeploymentNode(Deployment deployment, GroupNode deploymentGroup, int index) + { + var deploymentName = $"Deployment_{index}_{deployment.Category}"; + var deploymentNode = deploymentGroup.AddObject(new GroupNode(deploymentName) { IsEditable = false }); + return deploymentNode; + } + + private SceneNode CreateDeploymentZoneNode(DeploymentZone deploymentZone, SceneNode deploymentNode, int index) + { + var zoneName = $"DeploymentZone_{index}"; + var zoneNode = deploymentNode.AddObject(new GroupNode(zoneName) { IsEditable = false }); + return zoneNode; + } + + private SceneNode CreateDeploymentZoneRegionNode(DeploymentZoneRegion deploymentZoneRegion, SceneNode zoneNode, int index) + { + var regionName = $"DeploymentZoneRegion_{deploymentZoneRegion.Id}"; + var regionNode = zoneNode.AddObject(new GroupNode(regionName) { IsEditable = false }); + return regionNode; + } + + private SceneNode CreateBoundaryNode(Boundary boundary, SceneNode regionNode, int index) + { + var boundaryName = $"Boundary_{index}_{boundary.BoundaryType}"; + var boundaryNode = regionNode.AddObject(new GroupNode(boundaryName) { IsEditable = false }); + + var boundaryMesh = CreateSpecializedNode(() => new BoundaryNode("Boundary_Placeholder") + { + PointList = boundary.PointList + }); + if (boundaryMesh != null) + { + boundaryNode.AddObject(boundaryMesh); + } + + return boundaryNode; + } + + } + + // Special key class for BMD reference tracking to prevent infinite recursion + public class BmdBmdReferenceKey : BmdElementViewModel + { + public string BmdPath { get; } + + public BmdBmdReferenceKey(string bmdPath) : base("BMD_Reference", bmdPath, "BMD reference for recursion prevention") + { + BmdPath = bmdPath; + } + + public override bool Equals(object? obj) + { + return obj is BmdBmdReferenceKey other && BmdPath == other.BmdPath; + } + + public override int GetHashCode() + { + return BmdPath.GetHashCode(); + } + } +} diff --git a/Editors/BmdEditor/ViewModels/BmdEditorViewModel.cs b/Editors/BmdEditor/ViewModels/BmdEditorViewModel.cs new file mode 100644 index 000000000..64cf26151 --- /dev/null +++ b/Editors/BmdEditor/ViewModels/BmdEditorViewModel.cs @@ -0,0 +1,990 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using Shared.Core.Misc; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.ToolCreation; +using Shared.Core.Services; +using Shared.Core.ErrorHandling; +using Shared.GameFormats.Bmd; +using GameWorld.Core.Services; +using GameWorld.Core.Rendering.Materials; +using GameWorld.Core.Components; +using GameWorld.Core.Components.Selection; +using GameWorld.Core.SceneNodes; +using Serilog; +using Editors.BmdEditor.Services; // Added missing using directive + +namespace Editors.BmdEditor.ViewModels +{ + public class BmdEditorViewModel : NotifyPropertyChangedImpl, IEditorInterface, IFileEditor, IDisposable + { + private readonly IPackFileService _packFileService; + private readonly IEditorCreator _editorCreator; + private readonly IStandardDialogs _standardDialogs; + private readonly GameWorld.Core.Services.ResourceLibrary _resourceLibrary; + private readonly Shared.Core.Events.IEventHub _eventHub; + private readonly GameWorld.Core.Services.IGraphicsResourceCreator _graphicsResourceCreator; + private BmdFile? _bmdFile; + + public string DisplayName { get; set; } = "Not set"; + public PackFile CurrentFile { get; private set; } = null!; + public string StatusText { get; set; } = "Ready"; + + // BMD File Properties + public BmdFile? BmdFile + { + get => _bmdFile; + set => SetAndNotify(ref _bmdFile, value); + } + + // Collections for different element types + public ObservableCollection AllElements { get; } = new(); + public ObservableCollection BattlefieldBuildings { get; } = new(); + public ObservableCollection BattlefieldBuildingFars { get; } = new(); + public ObservableCollection CaptureLocations { get; } = new(); + public ObservableCollection EFLines { get; } = new(); + public ObservableCollection GoOutlines { get; } = new(); + public ObservableCollection NonTerrainOutlines { get; } = new(); + public ObservableCollection BuildingProjectileEmitters { get; } = new(); + public ObservableCollection ZonesTemplates { get; } = new(); + public ObservableCollection BmdInfos { get; } = new(); + public ObservableCollection Props { get; } = new(); + public ObservableCollection VfxInfos { get; } = new(); + public ObservableCollection PointLights { get; } = new(); + public ObservableCollection SpotLights { get; } = new(); + public ObservableCollection Sounds { get; } = new(); + public ObservableCollection PolyMeshes { get; } = new(); + public ObservableCollection LightProbes { get; } = new(); + public ObservableCollection TerrainHoles { get; } = new(); + public ObservableCollection PlayableAreas { get; } = new(); + public ObservableCollection CscInfos { get; } = new(); + public ObservableCollection Deployments { get; } = new(); + + // Commands + public ICommand RefreshCommand { get; } + public ICommand ExportCommand { get; } + public ICommand NavigateToReferencedFileCommand { get; } + + // Selected component for details display + private BmdElementViewModel? _selectedComponent; + public BmdElementViewModel? SelectedComponent + { + get => _selectedComponent; + set => SetAndNotify(ref _selectedComponent, value); + } + + // Component details for display + private string _componentDetails = "Select a component to view details"; + public string ComponentDetails + { + get => _componentDetails; + set => SetAndNotify(ref _componentDetails, value); + } + + private readonly BmdSceneCreator _bmdSceneCreator; + private readonly SelectionManager _selectionManager; + private readonly BmdElementLoader _bmdElementLoader; + + public IWpfGame Scene { get; set; } + + public BmdEditorViewModel( + IPackFileService packFileService, + IEditorCreator editorCreator, + IStandardDialogs standardDialogs, + MeshBuilderService meshBuilderService, + CapabilityMaterialFactory materialFactory, + GameWorld.Core.Services.ResourceLibrary resourceLibrary, + Shared.Core.Events.IEventHub eventHub, + GameWorld.Core.Services.IGraphicsResourceCreator graphicsResourceCreator, + IWpfGame gameWorld, + BmdSceneCreator bmdSceneCreator, + SelectionManager selectionManager, + IComponentInserter componentInserter, + BmdElementLoader bmdElementLoader) + { + _packFileService = packFileService; + _editorCreator = editorCreator; + _standardDialogs = standardDialogs; + _resourceLibrary = resourceLibrary; + _eventHub = eventHub; + _graphicsResourceCreator = graphicsResourceCreator; + _bmdSceneCreator = bmdSceneCreator; + _selectionManager = selectionManager; + _bmdElementLoader = bmdElementLoader; + + Scene = gameWorld; + + // Ensure all game components are added to the editor + componentInserter.Execute(); + + RefreshCommand = new RelayCommand(Refresh); + ExportCommand = new RelayCommand(Export); + NavigateToReferencedFileCommand = new RelayCommand(NavigateToReferencedFile); + + // Subscribe to selection changes from 3D view + _eventHub.Register(this, OnSelectionChanged); + System.Diagnostics.Debug.WriteLine("BmdEditorViewModel: Subscribed to SelectionChangedEvent"); + + if (_selectionManager == null) + { + System.Diagnostics.Debug.WriteLine("BmdEditorViewModel: WARNING - SelectionManager is null!"); + } + else + { + System.Diagnostics.Debug.WriteLine("BmdEditorViewModel: SelectionManager is properly initialized"); + } + } + + + + public void LoadFile(PackFile packFile) + { + CurrentFile = packFile; + DisplayName = packFile.Name; + + try + { + System.Diagnostics.Debug.WriteLine($"BMD Editor - Loading file: {packFile.Name}, Size: {packFile.DataSource.Size} bytes"); + + var data = packFile.DataSource.ReadData(); + System.Diagnostics.Debug.WriteLine($"BMD Editor - Read {data.Length} bytes from file"); + + using var stream = new MemoryStream(data); + var parser = new BmdParser(stream); + + BmdFile = parser.Parse(); + System.Diagnostics.Debug.WriteLine($"BMD Editor - Parsing completed successfully"); + + // Create 3D scene structure first + _bmdSceneCreator.CreateSceneFromBmd(BmdFile!, packFile); + System.Diagnostics.Debug.WriteLine($"BMD Editor - 3D scene structure created"); + + // Update collections (this will load scene content) + PopulateViewModels(); + System.Diagnostics.Debug.WriteLine($"BMD Editor - Collections and scene content loaded"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"BMD Editor - Failed to load BMD file {packFile.Name}: {ex.Message}"); + System.Diagnostics.Debug.WriteLine($"BMD Editor - Stack trace: {ex.StackTrace}"); + throw; + } + } + + private void PopulateViewModels() + { + if (BmdFile == null) return; + + // Clear existing collections + AllElements.Clear(); + BattlefieldBuildings.Clear(); + BattlefieldBuildingFars.Clear(); + CaptureLocations.Clear(); + EFLines.Clear(); + GoOutlines.Clear(); + NonTerrainOutlines.Clear(); + BuildingProjectileEmitters.Clear(); + ZonesTemplates.Clear(); + BmdInfos.Clear(); + Props.Clear(); + VfxInfos.Clear(); + PointLights.Clear(); + SpotLights.Clear(); + Sounds.Clear(); + PolyMeshes.Clear(); + LightProbes.Clear(); + TerrainHoles.Clear(); + PlayableAreas.Clear(); + CscInfos.Clear(); + Deployments.Clear(); + + // Use the BmdElementLoader to populate all collections + _bmdElementLoader.LoadElements(BmdFile, + AllElements, BmdInfos, BattlefieldBuildings, BattlefieldBuildingFars, + CaptureLocations, EFLines, GoOutlines, NonTerrainOutlines, + BuildingProjectileEmitters, ZonesTemplates, Props, VfxInfos, + PointLights, SpotLights, Sounds, PolyMeshes, LightProbes, + TerrainHoles, PlayableAreas, CscInfos, Deployments, + loadChildBmds: true); + } + + private void Refresh() + { + if (CurrentFile != null) + { + LoadFile(CurrentFile); + } + } + + private void Export() + { + // TODO: Implement export functionality + System.Diagnostics.Debug.WriteLine("Export BMD file - not yet implemented"); + } + + private void NavigateToReferencedFile(string? fileName) + { + if (string.IsNullOrEmpty(fileName)) return; + + try + { + // Try to find the referenced file in the pack file system + var referencedFile = _packFileService.FindFile(fileName); + if (referencedFile != null) + { + // Open the referenced file in the appropriate editor + var openCommand = new Shared.Core.Events.Global.OpenEditorCommand(_editorCreator, _packFileService); + openCommand.Execute(referencedFile); + } + else + { + System.Diagnostics.Debug.WriteLine($"Referenced file not found: {fileName}"); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to navigate to referenced file {fileName}: {ex.Message}"); + } + } + + public void SelectComponent(BmdElementViewModel component) + { + SelectedComponent = component; + ComponentDetails = GenerateComponentDetails(component); + + System.Diagnostics.Debug.WriteLine($"SelectComponent called: {component.ElementType} - {component.DisplayName}"); + + // Add visible debug info to component details + ComponentDetails += $"\n\n[DEBUG] SelectComponent called: {component.ElementType} - {component.DisplayName}"; + + // Select the component in the 3D scene using SelectionManager + SelectComponentIn3DScene(component); + + System.Diagnostics.Debug.WriteLine($"SelectComponent completed: {component.ElementType} - {component.DisplayName}"); + } + + private void SelectComponentIn3DScene(BmdElementViewModel component) + { + System.Diagnostics.Debug.WriteLine($"SelectComponentIn3DScene: Looking for component {component.ElementType} - {component.DisplayName}"); + + // Add visible debug info + ComponentDetails += $"\n[DEBUG] Looking for component: {component.ElementType} - {component.DisplayName}"; + + if (_bmdSceneCreator.ComponentNodes.TryGetValue(component, out var sceneNode)) + { + System.Diagnostics.Debug.WriteLine($"SelectComponentIn3DScene: Found scene node '{sceneNode.Name}' for component {component.ElementType} - {component.DisplayName}"); + ComponentDetails += $"\n[DEBUG] Found scene node: {sceneNode.Name}"; + + // Find the first selectable child node (Rmv2MeshNode implements ISelectable) + var selectableNode = FindFirstSelectableNode(sceneNode); + if (selectableNode != null) + { + System.Diagnostics.Debug.WriteLine($"SelectComponentIn3DScene: Found selectable node '{selectableNode.Name}' for component {component.ElementType} - {component.DisplayName}"); + ComponentDetails += $"\n[DEBUG] Found selectable node: {selectableNode.Name}"; + + // Clear current selection and select the new object + var objectSelection = _selectionManager.GetState(); + if (objectSelection != null) + { + objectSelection.Clear(); + objectSelection.ModifySelectionSingleObject(selectableNode, false); + System.Diagnostics.Debug.WriteLine($"SelectComponentIn3DScene: Successfully selected 3D object: {component.ElementType} - {component.DisplayName}"); + ComponentDetails += $"\n[DEBUG] Successfully selected 3D object!"; + } + else + { + System.Diagnostics.Debug.WriteLine($"SelectComponentIn3DScene: ObjectSelectionState is null!"); + ComponentDetails += $"\n[DEBUG] ERROR: ObjectSelectionState is null!"; + } + } + else + { + System.Diagnostics.Debug.WriteLine($"SelectComponentIn3DScene: No selectable node found for component: {component.ElementType} - {component.DisplayName}"); + ComponentDetails += $"\n[DEBUG] ERROR: No selectable node found!"; + } + } + else + { + System.Diagnostics.Debug.WriteLine($"SelectComponentIn3DScene: 3D object not found for component: {component.ElementType} - {component.DisplayName}"); + ComponentDetails += $"\n[DEBUG] ERROR: 3D object not found!"; + } + } + + private ISelectable? FindFirstSelectableNode(ISceneNode sceneNode) + { + // Check if the current node is selectable + if (sceneNode is ISelectable selectable) + return selectable; + + // Recursively search children + foreach (var child in sceneNode.Children) + { + var found = FindFirstSelectableNode(child); + if (found != null) + return found; + } + + return null; + } + + private string GenerateComponentDetails(BmdElementViewModel component) + { + var details = new System.Text.StringBuilder(); + details.AppendLine($"Type: {component.ElementType}"); + details.AppendLine($"Name: {component.DisplayName}"); + details.AppendLine($"Description: {component.Description}"); + details.AppendLine(); + + // Add type-specific details + switch (component) + { + case PropInfoViewModel prop: + details.AppendLine("Prop Details:"); + details.AppendLine($" Version: {prop.Prop.PropInfoVersion}"); + details.AppendLine($" File Path: {prop.PropFilePath}"); + details.AppendLine($" Is Decal: {prop.Prop.IsDecal}"); + details.AppendLine($" Animated: {prop.Prop.Animated}"); + details.AppendLine($" Casts Shadow: {prop.Prop.CastsShadow}"); + details.AppendLine($" RMV2 Path: {prop.Prop.Rmv2Path}"); + details.AppendLine($" Transform: {prop.Prop.Transform}"); + break; + + case VfxInfoViewModel vfx: + details.AppendLine("VFX Details:"); + details.AppendLine($" Version: {vfx.Vfx.VfxInfoVersion}"); + details.AppendLine($" VFX String: {vfx.Vfx.VfxString}"); + details.AppendLine($" Flag Version: {vfx.Vfx.FlagVersion}"); + details.AppendLine($" Allow In Outfield: {vfx.Vfx.AllowInOutfield}"); + details.AppendLine($" Clamp To Water: {vfx.Vfx.ClampToWaterSurface}"); + details.AppendLine($" Visible In Tactical: {vfx.Vfx.VisibleInTactical}"); + details.AppendLine($" Only Visible In Tactical: {vfx.Vfx.OnlyVisibleInTactical}"); + details.AppendLine($" Autoplay: {vfx.Vfx.Autoplay}"); + details.AppendLine($" Visible In Shroud: {vfx.Vfx.VisibleInShroud}"); + details.AppendLine($" Parent ID: {vfx.Vfx.ParentId}"); + break; + + case PointLightInfoViewModel light: + details.AppendLine("Point Light Details:"); + details.AppendLine($" Version: {light.Light.PointLightInfoVersion}"); + details.AppendLine($" Position: ({light.Light.Position.X:F2}, {light.Light.Position.Y:F2}, {light.Light.Position.Z:F2})"); + details.AppendLine($" Radius: {light.Light.Radius:F2}"); + details.AppendLine($" Color: ({light.Light.Red:F2}, {light.Light.Green:F2}, {light.Light.Blue:F2})"); + details.AppendLine($" Color Scale: {light.Light.ColorScale:F2}"); + details.AppendLine($" Animation Type: {light.Light.AnimationTypeEnum}"); + details.AppendLine($" Animation Speed 1: {light.Light.AnimationSpeedScale1:F2}"); + details.AppendLine($" Animation Speed 2: {light.Light.AnimationSpeedScale2:F2}"); + details.AppendLine($" Color Min: {light.Light.ColorMin:F2}"); + details.AppendLine($" Random Offset: {light.Light.RandomOffset:F2}"); + details.AppendLine($" WPLFT Type: {light.Light.WPLFTType}"); + details.AppendLine($" Height Mode: {light.Light.HeightMode}"); + details.AppendLine($" For Light Probe Only: {light.Light.ForLightProbeOnly}"); + break; + + case SpotLightInfoViewModel spotLight: + details.AppendLine("Spot Light Details:"); + details.AppendLine($" Version: {spotLight.Light.Version}"); + details.AppendLine($" Position: ({spotLight.Light.Position.X:F2}, {spotLight.Light.Position.Y:F2}, {spotLight.Light.Position.Z:F2})"); + details.AppendLine($" Length: {spotLight.Light.Length:F2}"); + details.AppendLine($" Inner Angle: {spotLight.Light.InnerAngleRadians:F2}"); + details.AppendLine($" Outer Angle: {spotLight.Light.OuterAngleRadians:F2}"); + details.AppendLine($" Color: ({spotLight.Light.IntensityRed:F2}, {spotLight.Light.IntensityGreen:F2}, {spotLight.Light.IntensityBlue:F2})"); + break; + + case SoundInfoViewModel sound: + details.AppendLine("Sound Details:"); + details.AppendLine($" Version: {sound.Sound.Version}"); + details.AppendLine($" Sound String: {sound.Sound.SoundString}"); + details.AppendLine($" Type: {sound.Sound.TypeString}"); + details.AppendLine($" Inner Radius: {sound.Sound.InnerRadius:F2}"); + details.AppendLine($" Outer Radius: {sound.Sound.OuterRadius:F2}"); + details.AppendLine($" Coord Count: {sound.Sound.CoordList.Length}"); + details.AppendLine($" Clamp To Surface: {sound.Sound.ClampToSurface}"); + details.AppendLine($" Height Mode: {sound.Sound.HeightMode}"); + details.AppendLine($" Campaign Type Mask: {sound.Sound.CampaignTypeMask}"); + details.AppendLine($" Direction Vector: ({sound.Sound.DirectionVector.X:F2}, {sound.Sound.DirectionVector.Y:F2}, {sound.Sound.DirectionVector.Z:F2})"); + details.AppendLine($" Up Vector: ({sound.Sound.UpVector.X:F2}, {sound.Sound.UpVector.Y:F2}, {sound.Sound.UpVector.Z:F2})"); + details.AppendLine($" Scope: {sound.Sound.Scope}"); + break; + + case PolyMeshInfoViewModel mesh: + details.AppendLine("PolyMesh Details:"); + details.AppendLine($" Version: {mesh.Mesh.PolyMeshVersion}"); + details.AppendLine($" Material: {mesh.Mesh.MaterialString}"); + details.AppendLine($" Vertices: {mesh.Mesh.VertexList.Length}"); + details.AppendLine($" Triangles: {mesh.Mesh.TriangleList.Length / 3}"); + details.AppendLine($" Visible In Tactical: {mesh.Mesh.VisibleInTactical}"); + break; + + case LightProbeInfoViewModel probe: + details.AppendLine("Light Probe Details:"); + details.AppendLine($" Version: {probe.Probe.Version}"); + details.AppendLine($" Position: ({probe.Probe.Position.X:F2}, {probe.Probe.Position.Y:F2}, {probe.Probe.Position.Z:F2})"); + details.AppendLine($" Inner Radius: {probe.Probe.InnerRadius:F2}"); + details.AppendLine($" Outer Radius: {probe.Probe.OuterRadius:F2}"); + details.AppendLine($" Primary: {probe.Probe.Primary}"); + details.AppendLine($" Height Mode: {probe.Probe.HeightMode}"); + break; + + case TerrainHoleInfoViewModel hole: + details.AppendLine("Terrain Hole Details:"); + details.AppendLine($" Version: {hole.Hole.TerrainHoleVersion}"); + details.AppendLine($" Position: ({hole.Hole.FirstVert.X:F2}, {hole.Hole.FirstVert.Y:F2}, {hole.Hole.FirstVert.Z:F2})"); + break; + + case CscInfoViewModel csc: + details.AppendLine("CSC Details:"); + details.AppendLine($" Version: {csc.Csc.Version}"); + details.AppendLine($" Scene File: {csc.Csc.SceneFile}"); + details.AppendLine($" Visible In Shroud: {csc.Csc.VisibleInShroud}"); + break; + + case BmdInfoViewModel bmd: + details.AppendLine("BMD Details:"); + details.AppendLine($" Version: {bmd.Bmd.Version}"); + details.AppendLine($" BMD String: {bmd.Bmd.BmdString}"); + details.AppendLine($" Region: {bmd.Bmd.RegionString}"); + details.AppendLine($" Height Mode: {bmd.Bmd.HeightMode}"); + break; + + case BattlefieldBuildingViewModel building: + details.AppendLine("Battlefield Building Details:"); + details.AppendLine($" Version: {building.Building.Version}"); + details.AppendLine($" Building Key: {building.Building.BuildingKey}"); + details.AppendLine($" Building Id: {building.Building.BuildingId}"); + details.AppendLine($" Position Type: {building.Building.PositionType}"); + details.AppendLine($" Height Mode: {building.Building.HeightMode}"); + details.AppendLine($" Parent Id: {building.Building.ParentId}"); + details.AppendLine($" Uid: {building.Building.Uid}"); + break; + + case BattlefieldBuildingFarViewModel buildingFar: + details.AppendLine("Battlefield Building Far Details:"); + details.AppendLine($" Version: {buildingFar.BuildingFar.Version}"); + break; + + case CaptureLocationViewModel captureLocation: + details.AppendLine("Capture Location Details:"); + details.AppendLine($" Version: {captureLocation.CaptureLocation.Version}"); + break; + + case EFLineViewModel efLine: + details.AppendLine("EF Line Details:"); + details.AppendLine($" Version: {efLine.EFLine.Version}"); + break; + + case GoOutlineViewModel goOutline: + details.AppendLine("GO Outline Details:"); + details.AppendLine($" Version: {goOutline.GoOutline.Version}"); + break; + + case NonTerrainOutlineViewModel nonTerrainOutline: + details.AppendLine("Non-Terrain Outline Details:"); + details.AppendLine($" Vertices: {nonTerrainOutline.NonTerrainOutline.VertexList.Count}"); + break; + + case BuildingProjectileEmitterViewModel emitter: + details.AppendLine("Building Projectile Emitter Details:"); + details.AppendLine($" Version: {emitter.BuildingProjectileEmitter.BuildingProjectileEmitterVersion}"); + details.AppendLine($" Location: ({emitter.BuildingProjectileEmitter.Location.X:F2}, {emitter.BuildingProjectileEmitter.Location.Y:F2}, {emitter.BuildingProjectileEmitter.Location.Z:F2})"); + details.AppendLine($" Key: {emitter.BuildingProjectileEmitter.SpecializedBuildingProjectileEmitterKey}"); + break; + + case ZonesTemplateViewModel zonesTemplate: + details.AppendLine("Zones Template Details:"); + details.AppendLine($" Version: {zonesTemplate.ZonesTemplate.Version}"); + details.AppendLine($" Outline Points: {zonesTemplate.ZonesTemplate.Outline.Count}"); + break; + + case PlayableAreaViewModel playableArea: + details.AppendLine("Playable Area Details:"); + details.AppendLine($" Version: {playableArea.PlayableArea.PlayableAreaVersion}"); + details.AppendLine($" Has Been Set: {playableArea.PlayableArea.HasBeenSet}"); + details.AppendLine($" Bounding Box: ({playableArea.PlayableArea.BoundingBox[0]:F2}, {playableArea.PlayableArea.BoundingBox[1]:F2}, {playableArea.PlayableArea.BoundingBox[2]:F2}, {playableArea.PlayableArea.BoundingBox[3]:F2})"); + details.AppendLine($" Flag Version: {playableArea.PlayableArea.FlagVersion}"); + details.AppendLine($" Flag 1: {playableArea.PlayableArea.Flag1}"); + details.AppendLine($" Flag 2: {playableArea.PlayableArea.Flag2}"); + details.AppendLine($" Flag 3: {playableArea.PlayableArea.Flag3}"); + details.AppendLine($" Flag 4: {playableArea.PlayableArea.Flag4}"); + break; + + case DeploymentViewModel deployment: + details.AppendLine("Deployment Details:"); + details.AppendLine($" Version: {deployment.Deployment.Version}"); + details.AppendLine($" Category: {deployment.Deployment.Category}"); + details.AppendLine($" Deployment Zones: {deployment.Deployment.DeploymentZones.Count}"); + break; + + case DeploymentZoneViewModel deploymentZone: + details.AppendLine("Deployment Zone Details:"); + details.AppendLine($" Version: {deploymentZone.DeploymentZone.Version}"); + details.AppendLine($" Deployment Zone Regions: {deploymentZone.DeploymentZone.DeploymentZoneRegions.Count}"); + break; + + case DeploymentZoneRegionViewModel deploymentZoneRegion: + details.AppendLine("Deployment Zone Region Details:"); + details.AppendLine($" Version: {deploymentZoneRegion.DeploymentZoneRegion.Version}"); + details.AppendLine($" Orientation: {deploymentZoneRegion.DeploymentZoneRegion.Orientation}"); + details.AppendLine($" Snap Facing: {deploymentZoneRegion.DeploymentZoneRegion.SnapFacing}"); + details.AppendLine($" Id: {deploymentZoneRegion.DeploymentZoneRegion.Id}"); + details.AppendLine($" Boundaries: {deploymentZoneRegion.DeploymentZoneRegion.Boundaries.Count}"); + break; + + case BoundaryViewModel boundary: + details.AppendLine("Boundary Details:"); + details.AppendLine($" Version: {boundary.Boundary.Version}"); + details.AppendLine($" Boundary Type: {boundary.Boundary.BoundaryType}"); + details.AppendLine($" Points: {boundary.Boundary.PointList.Count}"); + if (boundary.Boundary.PointList.Count > 0) + { + details.AppendLine(" First few points:"); + for (int i = 0; i < Math.Min(3, boundary.Boundary.PointList.Count); i++) + { + var point = boundary.Boundary.PointList[i]; + details.AppendLine($" Point {i + 1}: ({point.X:F2}, {point.Y:F2})"); + } + if (boundary.Boundary.PointList.Count > 3) + { + details.AppendLine($" ... and {boundary.Boundary.PointList.Count - 3} more points"); + } + } + break; + + default: + details.AppendLine("No additional details available for this component type."); + break; + } + + return details.ToString(); + } + + public void Close() + { + // Clear any active highlights + _bmdSceneCreator.ClearHighlight(); + + // Cleanup resources + AllElements.Clear(); + BattlefieldBuildings.Clear(); + BattlefieldBuildingFars.Clear(); + CaptureLocations.Clear(); + EFLines.Clear(); + GoOutlines.Clear(); + NonTerrainOutlines.Clear(); + BuildingProjectileEmitters.Clear(); + ZonesTemplates.Clear(); + BmdInfos.Clear(); + Props.Clear(); + VfxInfos.Clear(); + PointLights.Clear(); + SpotLights.Clear(); + Sounds.Clear(); + PolyMeshes.Clear(); + LightProbes.Clear(); + TerrainHoles.Clear(); + PlayableAreas.Clear(); + CscInfos.Clear(); + Deployments.Clear(); + } + + private void OnSelectionChanged(SelectionChangedEvent selectionEvent) + { + System.Diagnostics.Debug.WriteLine($"OnSelectionChanged called: {selectionEvent.NewState?.GetType().Name}"); + + if (selectionEvent.NewState is ObjectSelectionState objectSelection) + { + var selectedObject = objectSelection.GetSingleSelectedObject(); + System.Diagnostics.Debug.WriteLine($"OnSelectionChanged: Selected object is '{selectedObject?.Name ?? "null"}'"); + + if (selectedObject != null) + { + // Find the corresponding BMD element for this scene node + var component = FindComponentBySceneNode(selectedObject); + if (component != null) + { + // Update the selected component in the UI without triggering another selection change + SelectedComponent = component; + ComponentDetails = GenerateComponentDetails(component); + + // Add visible debug info + ComponentDetails += $"\n\n[DEBUG] 3D view selected: {component.ElementType} - {component.DisplayName}"; + + System.Diagnostics.Debug.WriteLine($"OnSelectionChanged: 3D view selected component: {component.ElementType} - {component.DisplayName}"); + } + else + { + System.Diagnostics.Debug.WriteLine($"OnSelectionChanged: No component found for selected object: {selectedObject.Name}"); + + // Show debug info even when no component is found + if (SelectedComponent != null) + { + ComponentDetails += $"\n\n[DEBUG] 3D view selected object: {selectedObject.Name} (no matching component found)"; + } + } + } + } + else + { + System.Diagnostics.Debug.WriteLine($"OnSelectionChanged: New state is not ObjectSelectionState"); + + if (SelectedComponent != null) + { + ComponentDetails += $"\n\n[DEBUG] Selection changed but not ObjectSelectionState: {selectionEvent.NewState?.GetType().Name}"; + } + } + } + + private BmdElementViewModel? FindComponentBySceneNode(ISelectable sceneNode) + { + System.Diagnostics.Debug.WriteLine($"FindComponentBySceneNode: Looking for scene node '{sceneNode.Name}'"); + + // Direct lookup using the ComponentNodes dictionary + foreach (var kvp in _bmdSceneCreator.ComponentNodes) + { + var component = kvp.Key; + var node = kvp.Value; + + // Check if this node or any of its children match the selected scene node + if (IsNodeOrDescendant(node, sceneNode)) + { + System.Diagnostics.Debug.WriteLine($"FindComponentBySceneNode: Found match: {component.ElementType} - {component.DisplayName}"); + return component; + } + } + + System.Diagnostics.Debug.WriteLine($"FindComponentBySceneNode: No matching component found for scene node '{sceneNode.Name}' or its descendants"); + return null; + } + + private bool IsNodeOrDescendant(ISceneNode node, ISelectable target) + { + // Check if the node itself is the target + if (node == target) + return true; + + // Check if any descendant matches the target + foreach (var child in node.Children) + { + if (IsNodeOrDescendant(child, target)) + return true; + } + + return false; + } + + + public void Dispose() + { + // Clear any active highlights + _bmdSceneCreator.ClearHighlight(); + + // Unregister from events + _eventHub?.UnRegister(this); + } + } + + // Base class for all BMD element view models + public abstract class BmdElementViewModel : NotifyPropertyChangedImpl + { + public string ElementType { get; } + public string DisplayName { get; } + public string Description { get; } + public virtual ObservableCollection Children { get; } = new(); + + protected BmdElementViewModel(string elementType, string displayName, string description = "") + { + ElementType = elementType; + DisplayName = displayName; + Description = description; + } + } + + // View models for specific element types + public class BattlefieldBuildingViewModel : BmdElementViewModel + { + public BattlefieldBuilding Building { get; } + + public BattlefieldBuildingViewModel(BattlefieldBuilding building) + : base("Battlefield Building", building.BuildingKey, $"Version: {building.Version}") + { + Building = building; + } + } + + public class PropInfoViewModel : BmdElementViewModel + { + public PropInfo Prop { get; } + public string PropName { get; } + public string PropFilePath { get; } + + public PropInfoViewModel(PropInfo prop, string propFilePath) + : base("Prop", System.IO.Path.GetFileNameWithoutExtension(propFilePath) ?? "Unknown Prop", $"Version: {prop.PropInfoVersion}") + { + Prop = prop; + PropName = System.IO.Path.GetFileNameWithoutExtension(propFilePath) ?? "Unknown Prop"; + PropFilePath = propFilePath; + } + } + + public class VfxInfoViewModel : BmdElementViewModel + { + public VfxInfo Vfx { get; } + + public VfxInfoViewModel(VfxInfo vfx) + : base("VFX", vfx.VfxString, $"Version: {vfx.VfxInfoVersion}") + { + Vfx = vfx; + } + } + + public class PointLightInfoViewModel : BmdElementViewModel + { + public PointLightInfo Light { get; } + + public PointLightInfoViewModel(PointLightInfo light) + : base("Point Light", $"Point Light at ({light.Position.X:F1}, {light.Position.Y:F1}, {light.Position.Z:F1})", + $"Radius: {light.Radius:F1}, Color: ({light.Red:F1}, {light.Green:F1}, {light.Blue:F1})") + { + Light = light; + } + } + + public class SpotLightInfoViewModel : BmdElementViewModel + { + public SpotLightInfo Light { get; } + + public SpotLightInfoViewModel(SpotLightInfo light) + : base("Spot Light", $"Spot Light at ({light.Position.X:F1}, {light.Position.Y:F1}, {light.Position.Z:F1})", + $"RGB: ({light.IntensityRed:F2},{light.IntensityGreen:F2},{light.IntensityBlue:F2}), Length: {light.Length:F2}") + { + Light = light; + } + } + + public class SoundInfoViewModel : BmdElementViewModel + { + public SoundInfo Sound { get; } + + public SoundInfoViewModel(SoundInfo sound) + : base("Sound", sound.SoundString, $"Type: {sound.TypeString}, Version: {sound.Version}") + { + Sound = sound; + } + } + + public class PolyMeshInfoViewModel : BmdElementViewModel + { + public PolyMeshInfo Mesh { get; } + + public PolyMeshInfoViewModel(PolyMeshInfo mesh) + : base("PolyMesh", mesh.MaterialString, $"Vertices: {mesh.VertexList.Length}, Triangles: {mesh.TriangleList.Length / 3}") + { + Mesh = mesh; + } + } + + public class LightProbeInfoViewModel : BmdElementViewModel + { + public LightProbeInfo Probe { get; } + + public LightProbeInfoViewModel(LightProbeInfo probe) + : base("Light Probe", $"Probe_{probe.Position.X:F2}_{probe.Position.Y:F2}_{probe.Position.Z:F2}", + $"Inner: {probe.InnerRadius:F2}, Outer: {probe.OuterRadius:F2}, Primary: {probe.Primary}") + { + Probe = probe; + } + } + + public class TerrainHoleInfoViewModel : BmdElementViewModel + { + public TerrainHoleTriangleInfo Hole { get; } + + public TerrainHoleInfoViewModel(TerrainHoleTriangleInfo hole) + : base("Terrain Hole", $"Hole at ({hole.FirstVert.X:F1}, {hole.FirstVert.Y:F1}, {hole.FirstVert.Z:F1})", + $"Version: {hole.TerrainHoleVersion}") + { + Hole = hole; + } + } + + public class CscInfoViewModel : BmdElementViewModel + { + public CscInfo Csc { get; } + + public CscInfoViewModel(CscInfo csc) + : base("CSC Info", csc.SceneFile, $"Version: {csc.Version}") + { + Csc = csc; + } + } + + public class BattlefieldBuildingFarViewModel : BmdElementViewModel + { + public BattlefieldBuildingFar BuildingFar { get; } + + public BattlefieldBuildingFarViewModel(BattlefieldBuildingFar buildingFar) + : base("Battlefield Building Far", "", $"Version: {buildingFar.Version}") + { + BuildingFar = buildingFar; + } + } + + public class CaptureLocationViewModel : BmdElementViewModel + { + public CaptureLocation CaptureLocation { get; } + + public CaptureLocationViewModel(CaptureLocation captureLocation) + : base("Capture Location", "", $"Version: {captureLocation.Version}") + { + CaptureLocation = captureLocation; + } + } + + public class EFLineViewModel : BmdElementViewModel + { + public EFLine EFLine { get; } + + public EFLineViewModel(EFLine efLine) + : base("EF Line", "", $"Version: {efLine.Version}") + { + EFLine = efLine; + } + } + + public class GoOutlineViewModel : BmdElementViewModel + { + public GoOutline GoOutline { get; } + + public GoOutlineViewModel(GoOutline goOutline) + : base("GO Outline", "", $"Version: {goOutline.Version}") + { + GoOutline = goOutline; + } + } + + public class NonTerrainOutlineViewModel : BmdElementViewModel + { + public NonTerrainOutline NonTerrainOutline { get; } + + public NonTerrainOutlineViewModel(NonTerrainOutline nonTerrainOutline) + : base("Non-Terrain Outline", "", $"Vertices: {nonTerrainOutline.VertexList?.Count ?? 0}") + { + NonTerrainOutline = nonTerrainOutline; + } + } + + public class BuildingProjectileEmitterViewModel : BmdElementViewModel + { + public BuildingProjectileEmitter BuildingProjectileEmitter { get; } + + public BuildingProjectileEmitterViewModel(BuildingProjectileEmitter buildingProjectileEmitter) + : base("Building Projectile Emitter", buildingProjectileEmitter.SpecializedBuildingProjectileEmitterKey, $"Version: {buildingProjectileEmitter.BuildingProjectileEmitterVersion}") + { + BuildingProjectileEmitter = buildingProjectileEmitter; + } + } + + public class ZonesTemplateViewModel : BmdElementViewModel + { + public ZonesTemplate ZonesTemplate { get; } + + public ZonesTemplateViewModel(ZonesTemplate zonesTemplate) + : base("Zones Template", "", $"Version: {zonesTemplate.Version}") + { + ZonesTemplate = zonesTemplate; + } + } + + public class PlayableAreaViewModel : BmdElementViewModel + { + public PlayableArea PlayableArea { get; } + + public PlayableAreaViewModel(PlayableArea playableArea) + : base("Playable Area", "", $"Version: {playableArea.PlayableAreaVersion}") + { + PlayableArea = playableArea; + } + } + + public class BmdInfoViewModel : BmdElementViewModel + { + public BmdInfo Bmd { get; } + public ObservableCollection ChildElements { get; } = new(); + public bool IsExpanded { get; set; } = false; + public bool HasChildren { get; set; } = false; + + public override ObservableCollection Children => ChildElements; + + public BmdInfoViewModel(BmdInfo bmd) + : base("BMD", bmd.BmdString, $"Version: {bmd.Version}, Region: {bmd.RegionString}") + { + Bmd = bmd; + } + } + + public class DeploymentZoneViewModel : BmdElementViewModel + { + public DeploymentZone DeploymentZone { get; } + + public DeploymentZoneViewModel(DeploymentZone deploymentZone, int index) + : base("Deployment Zone", $"Zone {index + 1}", $"Version: {deploymentZone.Version}, Regions: {deploymentZone.DeploymentZoneRegions.Count}") + { + DeploymentZone = deploymentZone; + + // Add child regions + foreach (var region in deploymentZone.DeploymentZoneRegions) + { + Children.Add(new DeploymentZoneRegionViewModel(region)); + } + } + } + + public class DeploymentZoneRegionViewModel : BmdElementViewModel + { + public DeploymentZoneRegion DeploymentZoneRegion { get; } + + public DeploymentZoneRegionViewModel(DeploymentZoneRegion deploymentZoneRegion) + : base("Deployment Zone Region", $"Region {deploymentZoneRegion.Id}", $"Version: {deploymentZoneRegion.Version}, Boundaries: {deploymentZoneRegion.Boundaries.Count}") + { + DeploymentZoneRegion = deploymentZoneRegion; + + // Add child boundaries + foreach (var boundary in deploymentZoneRegion.Boundaries) + { + Children.Add(new BoundaryViewModel(boundary)); + } + } + } + + public class BoundaryViewModel : BmdElementViewModel + { + public Boundary Boundary { get; } + + public BoundaryViewModel(Boundary boundary) + : base("Boundary", boundary.BoundaryType, $"Version: {boundary.Version}, Points: {boundary.PointList.Count}") + { + Boundary = boundary; + } + } + + public class DeploymentViewModel : BmdElementViewModel + { + public Deployment Deployment { get; } + + public DeploymentViewModel(Deployment deployment) + : base("Deployment", deployment.Category, $"Version: {deployment.Version}, Zones: {deployment.DeploymentZones.Count}") + { + Deployment = deployment; + + // Add child zones + for (int i = 0; i < deployment.DeploymentZones.Count; i++) + { + Children.Add(new DeploymentZoneViewModel(deployment.DeploymentZones[i], i)); + } + } + } +} diff --git a/Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs b/Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs new file mode 100644 index 000000000..51ad26100 --- /dev/null +++ b/Editors/BmdEditor/ViewModels/BmdSceneViewModel.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Windows.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Xna.Framework; +using Shared.Core.PackFiles; +using Shared.Core.PackFiles.Models; +using Shared.Core.ToolCreation; +using Shared.Core.Services; +using Shared.GameFormats.Bmd; +using Shared.GameFormats.RigidModel; +using GameWorld.Core.Components; +using GameWorld.Core.Rendering.Materials; +using GameWorld.Core.Services; +using GameWorld.Core.SceneNodes; +using Editors.BmdEditor.Services; +using Serilog; + +namespace Editors.BmdEditor.ViewModels +{ + public partial class BmdSceneViewModel : ObservableObject, IDisposable + { + private readonly ILogger _logger = Serilog.Log.ForContext(); + private readonly IPackFileService _packFileService; + private readonly IEditorCreator _editorCreator; + private readonly ResourceLibrary _resourceLibrary; + private readonly IGraphicsResourceCreator _graphicsResourceCreator; + private readonly MeshBuilderService _meshBuilderService; + private readonly CapabilityMaterialFactory _materialFactory; + private readonly BmdSceneCreator _bmdSceneCreator; + + private BmdFile? _bmdFile; + private IWpfGame? _scene3D; + + [ObservableProperty] string _statusText = "Ready"; + [ObservableProperty] string _sceneInfo = "No scene loaded"; + [ObservableProperty] int _loadedPropsCount = 0; + [ObservableProperty] bool _showGrid = true; + [ObservableProperty] bool _showProps = true; + [ObservableProperty] bool _showLights = true; + + [ObservableProperty] string _displayName = "BMD 3D Scene"; + + public IWpfGame? Scene3D + { + get => _scene3D; + private set => SetProperty(ref _scene3D, value); + } + + public ICommand ResetCameraCommand { get; } + public ICommand ToggleGridCommand { get; } + public ICommand TogglePropsCommand { get; } + public ICommand ToggleLightsCommand { get; } + + public BmdSceneViewModel( + IPackFileService packFileService, + IEditorCreator editorCreator, + ResourceLibrary resourceLibrary, + IGraphicsResourceCreator graphicsResourceCreator, + MeshBuilderService meshBuilderService, + CapabilityMaterialFactory materialFactory, + GameWorld.Core.Components.SceneManager sceneManager, + GameWorld.Core.SceneNodes.Rmv2ModelNodeLoader rmv2ModelNodeLoader) + { + _packFileService = packFileService; + _editorCreator = editorCreator; + _resourceLibrary = resourceLibrary; + _graphicsResourceCreator = graphicsResourceCreator; + _meshBuilderService = meshBuilderService; + _materialFactory = materialFactory; + _bmdSceneCreator = new BmdSceneCreator(packFileService, sceneManager, rmv2ModelNodeLoader, resourceLibrary, meshBuilderService); + + ResetCameraCommand = new RelayCommand(ResetCamera); + ToggleGridCommand = new RelayCommand(ToggleGrid); + TogglePropsCommand = new RelayCommand(ToggleProps); + ToggleLightsCommand = new RelayCommand(ToggleLights); + } + + public void LoadBmdFile(BmdFile bmdFile, PackFile packFile) + { + _bmdFile = bmdFile; + DisplayName = packFile.Name; + + try + { + _logger.Information($"Loading BMD file into 3D scene: {packFile.Name}"); + StatusText = "Loading 3D scene..."; + + // Use the BmdSceneCreator to create the scene + _bmdSceneCreator.CreateSceneFromBmd(bmdFile, packFile); + + // Update counts and info + LoadedPropsCount = Math.Min(bmdFile.Props.Count, bmdFile.PropInfos.Count); + UpdateSceneInfo(); + + StatusText = "Scene loaded successfully"; + _logger.Information($"BMD 3D scene loaded successfully with {LoadedPropsCount} props"); + } + catch (Exception ex) + { + _logger.Error($"Failed to load BMD file into 3D scene: {ex.Message}"); + StatusText = "Failed to load scene"; + throw; + } + } + + + private void UpdateSceneInfo() + { + var info = $"Props: {LoadedPropsCount}"; + if (_bmdFile != null) + { + info += $" | Lights: {_bmdFile.PointLights.Count + _bmdFile.SpotLights.Count}"; + info += $" | VFX: {_bmdFile.VfxInfos.Count}"; + } + SceneInfo = info; + } + + private void ResetCamera() + { + // TODO: Reset camera to default position + _logger.Information("Camera reset"); + } + + private void ToggleGrid() + { + ShowGrid = !ShowGrid; + // TODO: Show/hide grid in scene + _logger.Information($"Grid visibility: {ShowGrid}"); + } + + private void ToggleProps() + { + ShowProps = !ShowProps; + // TODO: Show/hide props in scene + _logger.Information($"Props visibility: {ShowProps}"); + } + + private void ToggleLights() + { + ShowLights = !ShowLights; + // TODO: Show/hide lights in scene + _logger.Information($"Lights visibility: {ShowLights}"); + } + + public void Dispose() + { + // Cleanup resources + Scene3D = null; + _bmdFile = null; + } + } +} diff --git a/Editors/BmdEditor/Views/Bmd3DSceneViewer.xaml b/Editors/BmdEditor/Views/Bmd3DSceneViewer.xaml new file mode 100644 index 000000000..7ccf9513a --- /dev/null +++ b/Editors/BmdEditor/Views/Bmd3DSceneViewer.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/Editors/BmdEditor/Views/Bmd3DSceneViewer.xaml.cs b/Editors/BmdEditor/Views/Bmd3DSceneViewer.xaml.cs new file mode 100644 index 000000000..37407c68e --- /dev/null +++ b/Editors/BmdEditor/Views/Bmd3DSceneViewer.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace Editors.BmdEditor.Views +{ + /// + /// Interaction logic for Bmd3DSceneViewer.xaml + /// + public partial class Bmd3DSceneViewer : UserControl + { + public Bmd3DSceneViewer() + { + InitializeComponent(); + } + } +} diff --git a/Editors/BmdEditor/Views/BmdEditorView.xaml b/Editors/BmdEditor/Views/BmdEditorView.xaml new file mode 100644 index 000000000..5d26ec688 --- /dev/null +++ b/Editors/BmdEditor/Views/BmdEditorView.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + +