diff --git a/Definitions/ObjectModels/Objects/Airport/AirportObject.cs b/Definitions/ObjectModels/Objects/Airport/AirportObject.cs index 131ff968..842e4014 100644 --- a/Definitions/ObjectModels/Objects/Airport/AirportObject.cs +++ b/Definitions/ObjectModels/Objects/Airport/AirportObject.cs @@ -16,7 +16,7 @@ public class AirportObject : ILocoStruct, IHasBuildingComponents public uint8_t var_07 { get; set; } public AirportObjectFlags Flags { get; set; } - public BuildingComponentsModel BuildingComponents { get; set; } = new(); + public BuildingComponents BuildingComponents { get; set; } = new(); public List BuildingPositions { get; set; } = []; public uint32_t LargeTiles { get; set; } public int8_t MinX { get; set; } diff --git a/Definitions/ObjectModels/Objects/Building/BuildingObject.cs b/Definitions/ObjectModels/Objects/Building/BuildingObject.cs index c92d187c..cb2411b5 100644 --- a/Definitions/ObjectModels/Objects/Building/BuildingObject.cs +++ b/Definitions/ObjectModels/Objects/Building/BuildingObject.cs @@ -7,7 +7,7 @@ namespace Definitions.ObjectModels.Objects.Building; public class BuildingObject : ILocoStruct, IHasBuildingComponents { - public BuildingComponentsModel BuildingComponents { get; set; } = new(); + public BuildingComponents BuildingComponents { get; set; } = new(); public uint32_t Colours { get; set; } public uint16_t DesignedYear { get; set; } public uint16_t ObsoleteYear { get; set; } diff --git a/Definitions/ObjectModels/Objects/Common/BuildingComponentsModel.cs b/Definitions/ObjectModels/Objects/Common/BuildingComponents.cs similarity index 73% rename from Definitions/ObjectModels/Objects/Common/BuildingComponentsModel.cs rename to Definitions/ObjectModels/Objects/Common/BuildingComponents.cs index 779ea53d..1963ac07 100644 --- a/Definitions/ObjectModels/Objects/Common/BuildingComponentsModel.cs +++ b/Definitions/ObjectModels/Objects/Common/BuildingComponents.cs @@ -1,14 +1,16 @@ using Definitions.ObjectModels.Validation; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Definitions.ObjectModels.Objects.Common; public interface IHasBuildingComponents { - BuildingComponentsModel BuildingComponents { get; set; } + BuildingComponents BuildingComponents { get; set; } } -public class BuildingComponentsModel : ILocoStruct +[TypeConverter(typeof(ExpandableObjectConverter))] +public class BuildingComponents : ILocoStruct { [Length(1, 63)] [CountEqualTo(nameof(BuildingAnimations))] @@ -43,6 +45,17 @@ public IEnumerable Validate(ValidationContext validationContex yield return new ValidationResult($"{nameof(BuildingVariations)} must contain between 1 and 31 entries.", [nameof(BuildingVariations)]); } + foreach (var bv in BuildingVariations) + { + foreach (var bvl in bv) + { + if (bvl >= BuildingHeights.Count) + { + yield return new ValidationResult($"A building variation layer index ({bvl}) is out of range. It must be less than the number of building heights ({BuildingHeights.Count}).", [nameof(BuildingVariations)]); + } + } + } + yield break; } } diff --git a/Definitions/ObjectModels/Objects/Dock/DockObject.cs b/Definitions/ObjectModels/Objects/Dock/DockObject.cs index 27fe9c4e..7d420660 100644 --- a/Definitions/ObjectModels/Objects/Dock/DockObject.cs +++ b/Definitions/ObjectModels/Objects/Dock/DockObject.cs @@ -11,7 +11,7 @@ public class DockObject : ILocoStruct, IHasBuildingComponents public uint8_t CostIndex { get; set; } public uint8_t var_07 { get; set; } // probably padding, not used in the game public DockObjectFlags Flags { get; set; } - public BuildingComponentsModel BuildingComponents { get; set; } = new(); + public BuildingComponents BuildingComponents { get; set; } = new(); public uint16_t DesignedYear { get; set; } public uint16_t ObsoleteYear { get; set; } public Pos2 BoatPosition { get; set; } diff --git a/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs b/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs index dc823fe8..1f957f34 100644 --- a/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs +++ b/Definitions/ObjectModels/Objects/Industry/IndustryObject.cs @@ -8,7 +8,7 @@ namespace Definitions.ObjectModels.Objects.Industry; public class IndustryObject : ILocoStruct, IHasBuildingComponents { public uint32_t FarmImagesPerGrowthStage { get; set; } - public BuildingComponentsModel BuildingComponents { get; set; } = new(); + public BuildingComponents BuildingComponents { get; set; } = new(); [Length(4, 4)] public List> AnimationSequences { get; set; } = []; // Access with getAnimationSequence helper method public List var_38 { get; set; } = []; // Access with getUnk38 helper method diff --git a/Gui/ViewModels/Graphics/ImageTableViewModel.cs b/Gui/ViewModels/Graphics/ImageTableViewModel.cs index 12bbd2c1..b92cc625 100644 --- a/Gui/ViewModels/Graphics/ImageTableViewModel.cs +++ b/Gui/ViewModels/Graphics/ImageTableViewModel.cs @@ -94,7 +94,7 @@ public class ImageTableViewModel : ReactiveObject, IExtraContentViewModel ImageTable Model { get; init; } - public ImageTableViewModel(ImageTable imageTable, ILogger logger, BuildingComponentsModel? buildingComponents = null) + public ImageTableViewModel(ImageTable imageTable, ILogger logger, BuildingComponents? buildingComponents = null) { ArgumentNullException.ThrowIfNull(imageTable); diff --git a/Gui/ViewModels/LocoTypes/ObjectEditorViewModel.cs b/Gui/ViewModels/LocoTypes/ObjectEditorViewModel.cs index 673d9306..029fafd0 100644 --- a/Gui/ViewModels/LocoTypes/ObjectEditorViewModel.cs +++ b/Gui/ViewModels/LocoTypes/ObjectEditorViewModel.cs @@ -256,22 +256,13 @@ public override void Load() else { CurrentObject.LocoObject.ImageTable?.PaletteMap = Model.PaletteMap; - - // temporary hack to show building components - if (CurrentObject.LocoObject.ObjectType == Definitions.ObjectModels.Types.ObjectType.Building) + if (CurrentObject.LocoObject.ImageTable == null) { - ExtraContentViewModel = new ImageTableViewModel(CurrentObject.LocoObject.ImageTable, Model.Logger, (CurrentObject.LocoObject.Object as IHasBuildingComponents)?.BuildingComponents); + logger.Info($"{CurrentFile.DisplayName} has no image table"); } else { - if (CurrentObject.LocoObject.ImageTable == null) - { - logger.Info($"{CurrentFile.DisplayName} has no image table"); - } - else - { - ExtraContentViewModel = new ImageTableViewModel(CurrentObject.LocoObject.ImageTable, Model.Logger, null); - } + ExtraContentViewModel = new ImageTableViewModel(CurrentObject.LocoObject.ImageTable, Model.Logger, (CurrentObject.LocoObject.Object as IHasBuildingComponents)?.BuildingComponents); } } } @@ -394,7 +385,7 @@ void SaveCore(string filename, SaveParameters saveParameters) logger.Info($"Saving {CurrentObject.DatInfo.S5Header.Name} to {filename}"); StringTableViewModel?.WriteTableBackToObject(); - // VM should auto-copy back now for everything but VehicleObject + // VM should auto-copy back now for everything but VehicleObject and BuildingObject CurrentObjectViewModel.CopyBackToModel(); // this is hacky but it should work diff --git a/Gui/ViewModels/LocoTypes/Objects/Building/BuildingComponentsViewModel.cs b/Gui/ViewModels/LocoTypes/Objects/Building/BuildingComponentsViewModel.cs index 3f342679..b873895a 100644 --- a/Gui/ViewModels/LocoTypes/Objects/Building/BuildingComponentsViewModel.cs +++ b/Gui/ViewModels/LocoTypes/Objects/Building/BuildingComponentsViewModel.cs @@ -25,8 +25,8 @@ public class BuildingComponentsViewModel : ReactiveObject [Reactive] public int MaxWidth { get; set; } [Reactive] public int MaxHeight { get; set; } - //[Reactive] - //public BuildingComponentsModel BuildingComponentsModel { get; set; } + [Reactive] + public BuildingComponents BuildingComponentsModel { get; set; } [Reactive, Browsable(false)] public ObservableCollection BuildingHeights { get; set; } = []; @@ -34,8 +34,8 @@ public class BuildingComponentsViewModel : ReactiveObject [Reactive, Browsable(false)] public ObservableCollection BuildingAnimations { get; set; } = []; - //[Reactive] - //public List> BuildingVariations { get; set; } = []; + [Reactive] + public List> BuildingVariations { get; set; } = []; //[Browsable(false)] [Reactive] @@ -45,40 +45,46 @@ public class BuildingComponentsViewModel : ReactiveObject public BuildingComponentsViewModel() { - //_ = this.WhenAnyValue(x => x.BuildingVariations) - // .Subscribe(_ => this.RaisePropertyChanged(nameof(BuildingVariationViewModels))); + _ = this.WhenAnyValue(x => x.BuildingVariations) + .Subscribe(_ => this.RaisePropertyChanged(nameof(BuildingVariationViewModels))); _ = this.WhenAnyValue(x => x.VerticalLayerSpacing) .Subscribe(ApplyOffsetToAllLayers); + + _ = MessageBus.Current.Listen().Subscribe(UpdateBuildingComponents); } - public BuildingComponentsViewModel(BuildingComponentsModel buildingComponents, ImageTable imageTable) : this() + public BuildingComponentsViewModel(BuildingComponents buildingComponents, ImageTable imageTable) : this() { ArgumentNullException.ThrowIfNull(buildingComponents); ArgumentNullException.ThrowIfNull(imageTable); - //_ = this.WhenAnyValue(x => x.BuildingVariationViewModels) - // .Where(x => x != null && ImageTable != null) - // .Subscribe(_ => RecomputeBuildingVariationViewModels(buildingComponents.BuildingVariations)); - ImageTable = imageTable; + UpdateBuildingComponents(buildingComponents); + } + + void UpdateBuildingComponents(BuildingComponents buildingComponents) + { + _ = this.WhenAnyValue(x => x.BuildingVariationViewModels) + .Where(x => x != null && ImageTable != null) + .Subscribe(_ => RecomputeBuildingVariationViewModels(buildingComponents.BuildingVariations, buildingComponents.BuildingHeights)); + BuildingHeights = new ObservableCollection(buildingComponents.BuildingHeights); BuildingAnimations = new ObservableCollection(buildingComponents.BuildingAnimations); + BuildingVariations = buildingComponents.BuildingVariations; + BuildingComponentsModel = buildingComponents; - //RecomputeBuildingVariationViewModels(buildingComponents.BuildingVariations); - - //BuildingVariations = buildingComponents.BuildingVariations; - //BuildingComponentsModel = buildingComponents; + RecomputeBuildingVariationViewModels(buildingComponents.BuildingVariations, buildingComponents.BuildingHeights); } - protected void RecomputeBuildingVariationViewModels(List> buildingVariations) + protected void RecomputeBuildingVariationViewModels(List> buildingVariations, List buildingHeights) { var layers = ImageTable.Groups.ConvertAll(x => x.GraphicsElements); BuildingVariationViewModels.Clear(); MaxWidth = layers.Max(x => x.Max(y => y.Width)) + 16; - MaxHeight = (layers.Max(x => x.Max(y => y.Height)) * BuildingHeights.Count) + BuildingHeights.Sum(x => x) + buildingVariations.Max(x => x.Count) * VerticalLayerSpacing; + MaxHeight = (layers.Max(x => x.Max(y => y.Height)) * buildingHeights.Count) + buildingHeights.Sum(x => x) + buildingVariations.Max(x => x.Count) * (VerticalLayerSpacing * 2); var x = 0; foreach (var variation in buildingVariations) @@ -99,18 +105,21 @@ protected void RecomputeBuildingVariationViewModels(List> building var cumulativeOffset = 0; foreach (var variationItem in variation) { - var layer = layers[variationItem]; - var bl = new BuildingLayerViewModel + if (layers.Count > variationItem) { - XBase = layer[i].XOffset, // + (MaxWidth / 2), - YBase = layer[i].YOffset - cumulativeOffset + MaxHeight * 0.80, - DisplayedImage = layer[i].Image.ToAvaloniaBitmap(), - XOffset = 0, - YOffset = 0, - }; - - cumulativeOffset += BuildingHeights[variationItem]; - bs.Layers.Add(bl); + var layer = layers[variationItem]; + var bl = new BuildingLayerViewModel + { + XBase = layer[i].XOffset + (MaxWidth / 2), + YBase = layer[i].YOffset - cumulativeOffset + MaxHeight * 0.80, + DisplayedImage = layer[i].Image.ToAvaloniaBitmap(), + XOffset = 0, + YOffset = 0, + }; + + cumulativeOffset += buildingHeights[variationItem]; + bs.Layers.Add(bl); + } } bv.Directions.Add(bs); // [i] = bs; diff --git a/Gui/ViewModels/LocoTypes/Objects/Building/BuildingViewModel.cs b/Gui/ViewModels/LocoTypes/Objects/Building/BuildingViewModel.cs index cf756b6e..b9454c19 100644 --- a/Gui/ViewModels/LocoTypes/Objects/Building/BuildingViewModel.cs +++ b/Gui/ViewModels/LocoTypes/Objects/Building/BuildingViewModel.cs @@ -5,15 +5,69 @@ using Definitions.ObjectModels.Types; using PropertyModels.ComponentModel.DataAnnotations; using PropertyModels.Extensions; +using ReactiveUI; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; namespace Gui.ViewModels.LocoTypes.Objects.Building; -public class BuildingViewModel(BuildingObject model) - : LocoObjectViewModel(model) +public class BuildingViewModel : LocoObjectViewModel { + public BuildingViewModel(BuildingObject model) : base(model) + { + ProducedCargo = new(model.ProducedCargo); + RequiredCargo = new(model.RequiredCargo); + ProducedQuantity = new(model.ProducedQuantity); + + BuildingVariations = new(model.BuildingComponents.BuildingVariations.Select(x => x.ToBindingList()).ToBindingList()); + BuildingHeights = model.BuildingComponents.BuildingHeights.ToBindingList(); + BuildingAnimations = model.BuildingComponents.BuildingAnimations.ToBindingList(); + + ElevatorSequence1 = model.ElevatorHeightSequences.Count > 0 ? new(model.ElevatorHeightSequences[0]) : null; + ElevatorSequence2 = model.ElevatorHeightSequences.Count > 1 ? new(model.ElevatorHeightSequences[1]) : null; + ElevatorSequence3 = model.ElevatorHeightSequences.Count > 2 ? new(model.ElevatorHeightSequences[2]) : null; + ElevatorSequence4 = model.ElevatorHeightSequences.Count > 3 ? new(model.ElevatorHeightSequences[3]) : null; + + // Subscribe to BuildingVariations changes (including nested lists) + BuildingVariations.ListChanged += OnBuildingComponentChanged; + foreach (var variation in BuildingVariations) + { + variation.ListChanged += OnBuildingComponentChanged; + } + + // Subscribe to BuildingHeights changes + BuildingHeights.ListChanged += OnBuildingComponentChanged; + + // Subscribe to BuildingAnimations changes + BuildingAnimations.ListChanged += OnBuildingComponentChanged; + } + public override void CopyBackToModel() + { + Model.BuildingComponents.BuildingVariations = [.. BuildingVariations.Select(x => x.ToList())]; + Model.BuildingComponents.BuildingHeights = [.. BuildingHeights]; + Model.BuildingComponents.BuildingAnimations = [.. BuildingAnimations]; + } + + void OnBuildingComponentChanged(object? sender, ListChangedEventArgs e) + { + // When a new nested list is added to BuildingVariations, subscribe to it + if (sender == BuildingVariations && e.ListChangedType == ListChangedType.ItemAdded) + { + BuildingVariations[e.NewIndex].ListChanged += OnBuildingComponentChanged; + } + + // 'live' updates are not needed + //CopyBackToModel(); + + MessageBus.Current.SendMessage(new BuildingComponents() + { + BuildingAnimations = [.. BuildingAnimations], + BuildingHeights = [.. BuildingHeights], + BuildingVariations = [.. BuildingVariations.Select(x => x.ToList())] + }); + } + [EnumProhibitValues(BuildingObjectFlags.None)] public BuildingObjectFlags Flags { @@ -87,33 +141,40 @@ public uint16_t SellCostFactor set => Model.SellCostFactor = value; } - [Category("Production"), Length(0, BuildingObjectLoader.Constants.MaxProducedCargoType)] public BindingList ProducedCargo { get; set; } = new(model.ProducedCargo); - [Category("Production"), Length(0, BuildingObjectLoader.Constants.MaxProducedCargoType)] public BindingList RequiredCargo { get; set; } = new(model.RequiredCargo); - [Category("Production"), Length(1, BuildingObjectLoader.Constants.MaxProducedCargoType)] public BindingList ProducedQuantity { get; set; } = new(model.ProducedQuantity); + [Category("Production"), Length(0, BuildingObjectLoader.Constants.MaxProducedCargoType)] public BindingList ProducedCargo { get; set; } + [Category("Production"), Length(0, BuildingObjectLoader.Constants.MaxProducedCargoType)] public BindingList RequiredCargo { get; set; } + [Category("Production"), Length(1, BuildingObjectLoader.Constants.MaxProducedCargoType)] public BindingList ProducedQuantity { get; set; } + + //[Category("Building")] + //public BuildingComponents BuildingComponents + //{ + // get => Model.BuildingComponents; + // set => Model.BuildingComponents = value; + //} [Category("Building"), Length(1, BuildingObjectLoader.Constants.BuildingVariationCount)] - public BindingList> BuildingVariations { get; init; } = new(model.BuildingComponents.BuildingVariations.Select(x => x.ToBindingList()).ToBindingList()); + public BindingList> BuildingVariations { get; init; } [Category("Building"), Length(1, BuildingObjectLoader.Constants.BuildingHeightCount)] - public BindingList BuildingHeights { get; init; } = model.BuildingComponents.BuildingHeights.ToBindingList(); + public BindingList BuildingHeights { get; init; } [Category("Building"), Length(1, BuildingObjectLoader.Constants.BuildingAnimationCount)] - public BindingList BuildingAnimations { get; init; } = model.BuildingComponents.BuildingAnimations.ToBindingList(); + public BindingList BuildingAnimations { get; init; } // note: these height sequences are massive. BLDCTY28 has 2 sequences, 512 in length and 1024 in length. Avalonia PropertyGrid takes 30+ seconds to render this. todo: don't use property grid in future //[Reactive, Category("Building"), Length(1, BuildingObject.MaxElevatorHeightSequences), Browsable(false)] public BindingList> ElevatorHeightSequences { get; set; } // NumElevatorSequences [Category("Elevator"), Browsable(false)] - public BindingList? ElevatorSequence1 { get; init; } = model.ElevatorHeightSequences.Count > 0 ? new(model.ElevatorHeightSequences[0]) : null; + public BindingList? ElevatorSequence1 { get; init; } [Category("Elevator"), Browsable(false)] - public BindingList? ElevatorSequence2 { get; init; } = model.ElevatorHeightSequences.Count > 1 ? new(model.ElevatorHeightSequences[1]) : null; + public BindingList? ElevatorSequence2 { get; init; } [Category("Elevator"), Browsable(false)] - public BindingList? ElevatorSequence3 { get; init; } = model.ElevatorHeightSequences.Count > 2 ? new(model.ElevatorHeightSequences[2]) : null; + public BindingList? ElevatorSequence3 { get; init; } [Category("Elevator"), Browsable(false)] - public BindingList? ElevatorSequence4 { get; init; } = model.ElevatorHeightSequences.Count > 3 ? new(model.ElevatorHeightSequences[3]) : null; + public BindingList? ElevatorSequence4 { get; init; } [Category("")] public uint8_t var_A6 diff --git a/Gui/ViewModels/LocoTypes/Objects/Building/DesignBuildingComponentsViewModel.cs b/Gui/ViewModels/LocoTypes/Objects/Building/DesignBuildingComponentsViewModel.cs index 0f938c21..daf29857 100644 --- a/Gui/ViewModels/LocoTypes/Objects/Building/DesignBuildingComponentsViewModel.cs +++ b/Gui/ViewModels/LocoTypes/Objects/Building/DesignBuildingComponentsViewModel.cs @@ -79,7 +79,8 @@ public DesignBuildingComponentsViewModel() [0, 1], [0, 1, 1], ]; + List buildingHeights = [16, 16]; - RecomputeBuildingVariationViewModels(buildingVariations); + RecomputeBuildingVariationViewModels(buildingVariations, buildingHeights); } } diff --git a/Gui/Views/BuildingComponentsView.axaml b/Gui/Views/BuildingComponentsView.axaml index 615a892a..2ce3f074 100644 --- a/Gui/Views/BuildingComponentsView.axaml +++ b/Gui/Views/BuildingComponentsView.axaml @@ -18,7 +18,7 @@ - + @@ -31,8 +31,7 @@ - - + @@ -60,20 +59,23 @@ - - - - - - - - - - - - + + + Building Component Viewer + + + + + + + + + + + + + + + - diff --git a/Gui/Views/ImageTableView.axaml b/Gui/Views/ImageTableView.axaml index b9d60bea..171d5088 100644 --- a/Gui/Views/ImageTableView.axaml +++ b/Gui/Views/ImageTableView.axaml @@ -203,8 +203,8 @@ - - + + @@ -246,10 +246,12 @@ - - - - + + + + + + - + @@ -297,9 +299,10 @@ - - + + +