diff --git a/DataQuery/DataQuery.csproj b/DataQuery/DataQuery.csproj deleted file mode 100644 index 02017247..00000000 --- a/DataQuery/DataQuery.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net9.0 - enable - enable - - - - - - - - diff --git a/DataQuery/Program.cs b/DataQuery/Program.cs deleted file mode 100644 index 08d46af5..00000000 --- a/DataQuery/Program.cs +++ /dev/null @@ -1,190 +0,0 @@ -// See https://aka.ms/new-console-template for more information -using OpenLoco.Common.Logging; -using Common.Logging; -using OpenLoco.Dat.Data; -using Common.Logging; -using OpenLoco.Dat.FileParsing; -using Common.Logging; -using OpenLoco.Dat.Objects; -using Common.Logging; -using OpenLoco.Definitions.Database; -using Common.Logging; -using System.Reflection; -using Common.Logging; - -var dir = "Q:\\Games\\Locomotion\\Server\\Objects"; -var logger = new Logger(); -var index = ObjectIndex.LoadOrCreateIndex(dir, logger); - -//QueryCostIndices(dir, logger, index); -//QueryCargoCategories(dir, logger, index); -//QueryVehicleBodyUnkSprites(dir, logger, index); -//QueryIndustryHasShadows(dir, logger, index); - -Console.WriteLine("done"); - -Console.ReadLine(); - -static void QueryIndustryHasShadows(string dir, Logger logger, ObjectIndex index) -{ - var results = new List<(ObjectIndexEntry Obj, ObjectSource ObjectSource)>(); - - foreach (var obj in index.Objects.Where(x => x.ObjectType == ObjectType.Industry)) - { - try - { - var o = SawyerStreamReader.LoadFullObjectFromFile(Path.Combine(dir, obj.FileName), logger); - if (o?.LocoObject != null) - { - var struc = (IndustryObject)o.Value.LocoObject.Object; - var header = o.Value.DatFileInfo.S5Header; - var source = OriginalObjectFiles.GetFileSource(header.Name, header.Checksum); - - if (struc.Flags.HasFlag(IndustryObjectFlags.HasShadows)) - { - results.Add((obj, source)); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"{obj.FileName} - {ex.Message}"); - } - } - - Console.WriteLine(results.Count); - - const string csvHeader = "DatName, ObjectSource"; - var lines = results - .OrderBy(x => x.Obj.DisplayName) - .Select(x => string.Join(',', x.Obj.DisplayName, x.ObjectSource)); - - File.WriteAllLines("vehicleBodiesWithUnkSpritesFlag.csv", [csvHeader, .. lines]); -} - -static void QueryVehicleBodyUnkSprites(string dir, Logger logger, ObjectIndex index) -{ - var results = new List<(ObjectIndexEntry Obj, ObjectSource ObjectSource)>(); - - foreach (var obj in index.Objects.Where(x => x.ObjectType == ObjectType.Vehicle)) - { - try - { - var o = SawyerStreamReader.LoadFullObjectFromFile(Path.Combine(dir, obj.FileName), logger); - if (o?.LocoObject != null) - { - var struc = (VehicleObject)o.Value.LocoObject.Object; - var header = o.Value.DatFileInfo.S5Header; - var source = OriginalObjectFiles.GetFileSource(header.Name, header.Checksum); - - if (struc.Flags.HasFlag(VehicleObjectFlags.AlternatingCarSprite)) - { - results.Add((obj, source)); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"{obj.FileName} - {ex.Message}"); - } - } - - Console.WriteLine(results.Count); - - const string csvHeader = "DatName, ObjectSource"; - var lines = results - .OrderBy(x => x.Obj.DisplayName) - .Select(x => string.Join(',', x.Obj.DisplayName, x.ObjectSource)); - - File.WriteAllLines("vehicleBodiesWithUnkSpritesFlag.csv", [csvHeader, .. lines]); -} - -static void QueryCargoCategories(string dir, Logger logger, ObjectIndex index) -{ - var results = new List<(ObjectIndexEntry Obj, CargoCategory CargoCategory, string LocalisedName, ObjectSource ObjectSource)>(); - - foreach (var obj in index.Objects.Where(x => x.ObjectType == ObjectType.Cargo)) - { - try - { - var o = SawyerStreamReader.LoadFullObjectFromFile(Path.Combine(dir, obj.FileName), logger); - if (o?.LocoObject != null) - { - var struc = (CargoObject)o.Value.LocoObject.Object; - - var header = o.Value.DatFileInfo.S5Header; - var source = OriginalObjectFiles.GetFileSource(header.Name, header.Checksum); - - results.Add((obj, struc.CargoCategory, o.Value.LocoObject.StringTable.Table["Name"][LanguageId.English_UK], source)); - } - } - catch (Exception ex) - { - Console.WriteLine($"{obj.FileName} - {ex.Message}"); - } - } - - Console.WriteLine("writing to file"); - - const string csvHeader = "DatName, CargoCategory, LocalisedName, ObjectSource"; - var lines = results - .OrderBy(x => x.Obj.DisplayName) - .Select(x => string.Join(',', x.Obj.DisplayName, (int)x.CargoCategory, x.LocalisedName, x.ObjectSource)); - File.WriteAllLines("cargoCategories.csv", [csvHeader, .. lines]); -} - -static void QueryCostIndices(string dir, Logger logger, ObjectIndex index) -{ - var results = new List<(ObjectIndexEntry Obj, byte CostIndex, short? RunCostIndex)>(); - - foreach (var obj in index.Objects) - { - try - { - var o = SawyerStreamReader.LoadFullObjectFromFile(Path.Combine(dir, obj.FileName), logger); - if (o?.LocoObject != null) - { - var struc = o.Value.LocoObject.Object; - var type = struc.GetType(); - - var costIndexProperty = type.GetProperty("CostIndex", BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - var paymentIndexProperty = type.GetProperty("PaymentIndex", BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - var runCostIndexProperty = type.GetProperty("RunCostIndex", BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); - - byte? costIndex = null; - byte? runCostIndex = null; - - if (costIndexProperty?.PropertyType == typeof(byte) && costIndexProperty.GetValue(struc) is byte costIndexValue) - { - costIndex = costIndexValue; - } - else if (paymentIndexProperty?.PropertyType == typeof(byte) && paymentIndexProperty.GetValue(struc) is byte paymentIndexValue) - { - costIndex = paymentIndexValue; - } - - if (runCostIndexProperty?.PropertyType == typeof(byte) && runCostIndexProperty.GetValue(struc) is byte runCostIndexValue) - { - runCostIndex = runCostIndexValue; - } - - if (costIndex != null) - { - results.Add((obj, costIndex.Value, runCostIndex)); - } - } - } - catch (Exception ex) - { - Console.WriteLine($"{obj.FileName} - {ex.Message}"); - } - } - - Console.WriteLine("writing to file"); - - const string header = "DatName, ObjectType, CostIndex, RunCostIndex"; - var lines = results - .OrderBy(x => x.Obj.DisplayName) - .Select(x => string.Join(',', x.Obj.DisplayName, x.Obj.ObjectType, x.CostIndex, x.RunCostIndex)); - File.WriteAllLines("costIndex.csv", [header, .. lines]); -} diff --git a/Definitions/Definitions.csproj b/Definitions/Definitions.csproj index 1d3364be..90a994c0 100644 --- a/Definitions/Definitions.csproj +++ b/Definitions/Definitions.csproj @@ -63,6 +63,12 @@ + + + PreserveNewest + + + diff --git a/Definitions/ObjectModels/Graphics/ImageTableGroupConfiguration.cs b/Definitions/ObjectModels/Graphics/ImageTableGroupConfiguration.cs new file mode 100644 index 00000000..256ea8ee --- /dev/null +++ b/Definitions/ObjectModels/Graphics/ImageTableGroupConfiguration.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Definitions.ObjectModels.Graphics; + +internal sealed record ImageTableGroupDefinition( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("start")] int Start, + [property: JsonPropertyName("chunkSize")] int? ChunkSize = null +); + +internal sealed record ImageTableGroupConfiguration( + [property: JsonPropertyName("objectType")] string ObjectType, + [property: JsonPropertyName("groups")] List Groups +); diff --git a/Definitions/ObjectModels/Graphics/ImageTableGrouper.cs b/Definitions/ObjectModels/Graphics/ImageTableGrouper.cs index 98e4463d..a2db98a6 100644 --- a/Definitions/ObjectModels/Graphics/ImageTableGrouper.cs +++ b/Definitions/ObjectModels/Graphics/ImageTableGrouper.cs @@ -1,7 +1,10 @@ +using Common.Json; using Definitions.ObjectModels.Objects.Competitor; using Definitions.ObjectModels.Objects.Vehicle; using Definitions.ObjectModels.Types; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; namespace Definitions.ObjectModels.Graphics; @@ -28,12 +31,22 @@ public static ImageTable CreateImageTable(ILocoStruct obj, ObjectType objectType return imageTable; } +public static IEnumerable CreateGroupsForExistingImages(ILocoStruct obj, ObjectType objectType, List imageList) +{ + var originalCount = imageList.Count; + var groups = CreateGroups(obj, objectType, imageList).ToList(); + + Debug.Assert(groups.SelectMany(g => g.GraphicsElements).Count() == originalCount, "Image grouping lost or gained images"); + + return groups; +} + private static IEnumerable CreateGroups(ILocoStruct obj, ObjectType objectType, List imageList) { switch (objectType) { case ObjectType.InterfaceSkin: - return CreateInterfaceGroups(imageList); + return CreateGroupsFromConfig(ObjectType.InterfaceSkin, imageList); case ObjectType.Sound: return [new("", [.. imageList])]; case ObjectType.Currency: @@ -41,15 +54,15 @@ private static IEnumerable CreateGroups(ILocoStruct obj, Object case ObjectType.Steam: return [new("", [.. imageList])]; case ObjectType.CliffEdge: - return CreateCliffEdgeGroups(imageList); + return CreateGroupsFromConfig(ObjectType.CliffEdge, imageList); case ObjectType.Water: - return CreateWaterGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Water, imageList); case ObjectType.Land: return [new("", [.. imageList])]; case ObjectType.TownNames: return [new("", [.. imageList])]; case ObjectType.Cargo: - return CreateCargoGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Cargo, imageList); case ObjectType.Wall: return [new("", [.. imageList])]; case ObjectType.TrackSignal: @@ -57,11 +70,11 @@ private static IEnumerable CreateGroups(ILocoStruct obj, Object case ObjectType.LevelCrossing: return [new("", [.. imageList])]; case ObjectType.StreetLight: - return CreateStreetLightGroups(imageList); + return CreateGroupsFromConfig(ObjectType.StreetLight, imageList); case ObjectType.Tunnel: return [new("", [.. imageList])]; case ObjectType.Bridge: - return CreateBridgeGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Bridge, imageList); case ObjectType.TrackStation: return [new("", [.. imageList])]; case ObjectType.TrackExtra: @@ -75,13 +88,13 @@ private static IEnumerable CreateGroups(ILocoStruct obj, Object case ObjectType.Road: return [new("", [.. imageList])]; case ObjectType.Airport: - return CreateAirportGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Airport, imageList); case ObjectType.Dock: - return CreateDockGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Dock, imageList); case ObjectType.Vehicle: return CreateVehicleGroups((VehicleObject)obj, imageList); case ObjectType.Tree: - return CreateTreeGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Tree, imageList); case ObjectType.Snow: return [new("", [.. imageList])]; case ObjectType.Climate: @@ -89,9 +102,9 @@ private static IEnumerable CreateGroups(ILocoStruct obj, Object case ObjectType.HillShapes: return [new("", [.. imageList])]; case ObjectType.Building: - return CreateBuildingGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Building, imageList); case ObjectType.Industry: - return CreateBuildingGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Industry, imageList); case ObjectType.Region: return [new("", [.. imageList])]; case ObjectType.Competitor: @@ -99,53 +112,116 @@ private static IEnumerable CreateGroups(ILocoStruct obj, Object case ObjectType.ScenarioText: return [new("", [.. imageList])]; case ObjectType.Scaffolding: - return CreateScaffoldingGroups(imageList); + return CreateGroupsFromConfig(ObjectType.Scaffolding, imageList); default: return []; } } - private static IEnumerable CreateAirportGroups(List imageList) + private static IEnumerable CreateGroupsFromConfig(ObjectType objectType, List imageList) { - yield return new("preview", imageList[0..1]); - - foreach (var group in imageList - .Skip(1) - .Chunk(4) - .Select((x, i) => new ImageTableGroup($"Part {i}", [.. x]))) + if (TryGetGroupConfiguration(objectType, out var configuration)) { - yield return group; + return CreateGroupsFromConfig(configuration, imageList); } + + return [new("", [.. imageList])]; } - private static IEnumerable CreateBridgeGroups(List imageList) + private static bool TryGetGroupConfiguration(ObjectType objectType, [NotNullWhen(true)] out ImageTableGroupConfiguration? configuration) { - yield return new("preview", imageList[0..1]); - yield return new("base plates", imageList[1..6]); - yield return new("unk", imageList[6..12]); - yield return new("", imageList[12..]); + configuration = null; + return GroupConfigurations.TryGetValue(objectType, out configuration); } - private static IEnumerable CreateBuildingGroups(List imageList) - => imageList - .Chunk(4) - .Select((x, i) => new ImageTableGroup($"Part {i}", [.. x])); - - private static IEnumerable CreateCargoGroups(List imageList) + private static IEnumerable CreateGroupsFromConfig(ImageTableGroupConfiguration configuration, List imageList) { - yield return new("preview", imageList[0..1]); - yield return new("station variations", imageList[1..]); + var groups = configuration.Groups.OrderBy(group => group.Start).ToList(); + for (var index = 0; index < groups.Count; index++) + { + var current = groups[index]; + var nextStart = index + 1 < groups.Count + ? groups[index + 1].Start + : imageList.Count; + + if (current.Start < 0) + { + continue; + } + + if (current.Start >= imageList.Count) + { + break; // no images remain for this or later groups + } + + var actualEnd = Math.Min(nextStart, imageList.Count); + if (actualEnd <= current.Start) + { + continue; // no images for this group + } + + if (current.ChunkSize is null) + { + if (nextStart > imageList.Count) + { + yield return new("", imageList[current.Start..actualEnd]); + break; + } + + yield return new(current.Name, imageList[current.Start..actualEnd]); + continue; + } + + if (current.ChunkSize <= 0) + { + continue; + } + + var actualChunkSize = current.ChunkSize.Value; + var chunkIndex = 0; + for (var chunkStart = current.Start; chunkStart < actualEnd; chunkStart += actualChunkSize) + { + var chunkEnd = Math.Min(chunkStart + actualChunkSize, actualEnd); + var chunkName = current.Name.Contains("{i}") + ? current.Name.Replace("{i}", chunkIndex.ToString()) + : current.Name; + + yield return new(chunkName, imageList[chunkStart..chunkEnd]); + chunkIndex++; + } + + if (nextStart > imageList.Count) + { + break; + } + } } - private static IEnumerable CreateCliffEdgeGroups(List imageList) + public static void LoadGroupConfigurationFile(string configFilePath) { - yield return new("left west", imageList[0..16]); - yield return new("right east", imageList[16..32]); - yield return new("right west", imageList[32..48]); - yield return new("left east", imageList[48..64]); - yield return new("far-side slopes", imageList[64..]); + if (string.IsNullOrEmpty(configFilePath) || !File.Exists(configFilePath)) + { + GroupConfigurations = new Dictionary(); + return; + } + + try + { + var json = File.ReadAllText(configFilePath); + var configurations = JsonSerializer.Deserialize>(json, JsonFile.DefaultSerializerOptions) ?? []; + GroupConfigurations = configurations + .Select(configuration => (configuration, success: Enum.TryParse(configuration.ObjectType, ignoreCase: true, out var objectType), objectType)) + .Where(pair => pair.success) + .ToDictionary(pair => pair.objectType, pair => pair.configuration); + } + catch (JsonException) + { + GroupConfigurations = new Dictionary(); + } } + private static IReadOnlyDictionary GroupConfigurations = new Dictionary(); + private static IEnumerable CreateCompetitorGroups(CompetitorObject model, List imageList) { var offset = 0; @@ -352,95 +428,4 @@ private static IEnumerable CreateVehicleGroups(VehicleObject mo } } - private static IEnumerable CreateDockGroups(List imageList) - { - yield return new("preview", [imageList[0]]); - - foreach (var group in imageList - .Skip(1) - .Chunk(4) - .Select((x, i) => new ImageTableGroup($"Part {i}", [.. x]))) - { - yield return group; - } - } - - private static IEnumerable CreateInterfaceGroups(List imageList) - { - yield return new("preview", imageList[0..1]); - yield return new("toolbar", imageList[1..31]); - yield return new("build-vehicle", imageList[31..43]); - yield return new("toolbar", imageList[43..49]); - yield return new("paint", imageList[49..57]); - yield return new("population", imageList[57..65]); - yield return new("performance-index", imageList[65..73]); - yield return new("cargo-units", imageList[73..81]); - yield return new("cargo-distance", imageList[81..89]); - yield return new("production", imageList[89..97]); - yield return new("wrench", imageList[97..113]); - yield return new("finances", imageList[113..129]); - yield return new("cup", imageList[129..145]); - yield return new("ratings", imageList[145..161]); - yield return new("transported", imageList[161..168]); - yield return new("cogs", imageList[168..172]); - yield return new("toolbar", imageList[172..203]); - yield return new("tab-train", imageList[203..211]); - yield return new("tab-aircraft", imageList[211..219]); - yield return new("tab-bus", imageList[219..227]); - yield return new("tab-tram", imageList[227..235]); - yield return new("tab-truck", imageList[235..243]); - yield return new("tab-ship", imageList[243..251]); - yield return new("build-train", imageList[251..267]); - yield return new("build-aircraft", imageList[267..283]); - yield return new("build-bus", imageList[283..299]); - yield return new("build-tram", imageList[299..315]); - yield return new("build-truck", imageList[315..331]); - yield return new("build-ship", imageList[331..347]); - yield return new("build-industry", imageList[347..363]); - yield return new("build-town", imageList[363..379]); - yield return new("build-buildings", imageList[379..395]); - yield return new("build-misc-buildings", imageList[395..411]); - yield return new("build-extra", imageList[411..418]); - yield return new("train", imageList[418..426]); - yield return new("aircraft", imageList[426..434]); - yield return new("bus", imageList[434..442]); - yield return new("tram", imageList[442..450]); - yield return new("truck", imageList[450..458]); - yield return new("ship", imageList[458..466]); - yield return new("toolbar-map", imageList[466..470]); - - // custom images added by OG - yield return new("high-res-logo", imageList[470..471]); - yield return new("blueprints", imageList[471..490]); - yield return new("", imageList[490..]); - } - - private static IEnumerable CreateScaffoldingGroups(List imageList) - { - yield return new("type 0", imageList[0..10]); - yield return new("type 1", imageList[10..24]); - yield return new("type 2", imageList[24..36]); - } - - private static IEnumerable CreateStreetLightGroups(List imageList) - => imageList - .Chunk(4) - .Select((x, i) => new ImageTableGroup($"Year group {i}", [.. x])); - - private static IEnumerable CreateTreeGroups(List imageList) - => imageList - .Chunk(4) - .Select((x, i) => new ImageTableGroup($"Variation {i}", [.. x])); - - private static IEnumerable CreateWaterGroups(List imageList) - { - yield return new("zoom 1", imageList[0..10]); - yield return new("zoom 2", imageList[10..20]); - yield return new("zoom 3", imageList[20..30]); - yield return new("zoom 4", imageList[30..40]); - yield return new("palettes", imageList[40..42]); - yield return new("icon-animation", imageList[42..58]); - yield return new("icon-interaction", imageList[58..60]); - yield return new("animation", imageList[60..76]); - } } diff --git a/Definitions/ObjectModels/Graphics/ImageTableGroups.json b/Definitions/ObjectModels/Graphics/ImageTableGroups.json new file mode 100644 index 00000000..81e57966 --- /dev/null +++ b/Definitions/ObjectModels/Graphics/ImageTableGroups.json @@ -0,0 +1,130 @@ +[ + { + "objectType": "InterfaceSkin", + "groups": [ + { "name": "preview", "start": 0 }, + { "name": "toolbar", "start": 1 }, + { "name": "build-vehicle", "start": 31 }, + { "name": "toolbar", "start": 43 }, + { "name": "paint", "start": 49 }, + { "name": "population", "start": 57 }, + { "name": "performance-index", "start": 65 }, + { "name": "cargo-units", "start": 73 }, + { "name": "cargo-distance", "start": 81 }, + { "name": "production", "start": 89 }, + { "name": "wrench", "start": 97 }, + { "name": "finances", "start": 113 }, + { "name": "cup", "start": 129 }, + { "name": "ratings", "start": 145 }, + { "name": "transported", "start": 161 }, + { "name": "cogs", "start": 168 }, + { "name": "toolbar", "start": 172 }, + { "name": "tab-train", "start": 203 }, + { "name": "tab-aircraft", "start": 211 }, + { "name": "tab-bus", "start": 219 }, + { "name": "tab-tram", "start": 227 }, + { "name": "tab-truck", "start": 235 }, + { "name": "tab-ship", "start": 243 }, + { "name": "build-train", "start": 251 }, + { "name": "build-aircraft", "start": 267 }, + { "name": "build-bus", "start": 283 }, + { "name": "build-tram", "start": 299 }, + { "name": "build-truck", "start": 315 }, + { "name": "build-ship", "start": 331 }, + { "name": "build-industry", "start": 347 }, + { "name": "build-town", "start": 363 }, + { "name": "build-buildings", "start": 379 }, + { "name": "build-misc-buildings", "start": 395 }, + { "name": "build-extra", "start": 411 }, + { "name": "train", "start": 418 }, + { "name": "aircraft", "start": 426 }, + { "name": "bus", "start": 434 }, + { "name": "tram", "start": 442 }, + { "name": "truck", "start": 450 }, + { "name": "ship", "start": 458 }, + { "name": "toolbar-map", "start": 466 }, + { "name": "high-res-logo", "start": 470 }, + { "name": "blueprints", "start": 471 }, + { "name": "", "start": 490 } + ] + }, + { + "objectType": "Water", + "groups": [ + { "name": "zoom 1", "start": 0 }, + { "name": "zoom 2", "start": 10 }, + { "name": "zoom 3", "start": 20 }, + { "name": "zoom 4", "start": 30 }, + { "name": "palettes", "start": 40 }, + { "name": "icon-animation", "start": 42 }, + { "name": "icon-interaction", "start": 58 }, + { "name": "animation", "start": 60 }, + { "name": "", "start": 76 } + ] + }, + { + "objectType": "Bridge", + "groups": [ + { "name": "preview", "start": 0 }, + { "name": "base plates", "start": 1 }, + { "name": "unk", "start": 6 }, + { "name": "", "start": 12 } + ] + }, + { + "objectType": "Cargo", + "groups": [ + { "name": "preview", "start": 0 }, + { "name": "station variations", "start": 1 } + ] + }, + { + "objectType": "CliffEdge", + "groups": [ + { "name": "left west", "start": 0 }, + { "name": "right east", "start": 16 }, + { "name": "right west", "start": 32 }, + { "name": "left east", "start": 48 }, + { "name": "far-side slopes", "start": 64 } + ] + }, + { + "objectType": "StreetLight", + "groups": [{ "name": "Year group {i}", "start": 0, "chunkSize": 4 }] + }, + { + "objectType": "Tree", + "groups": [{ "name": "Variation {i}", "start": 0, "chunkSize": 4 }] + }, + { + "objectType": "Dock", + "groups": [ + { "name": "preview", "start": 0 }, + { "name": "Part {i}", "start": 1, "chunkSize": 4 } + ] + }, + { + "objectType": "Airport", + "groups": [ + { "name": "preview", "start": 0 }, + { "name": "Part {i}", "start": 1, "chunkSize": 4 } + ] + }, + { + "objectType": "Building", + "groups": [{ "name": "Part {i}", "start": 0, "chunkSize": 4 }] + }, + { + "objectType": "Industry", + "groups": [{ "name": "Part {i}", "start": 0, "chunkSize": 4 }] + }, + { + "objectType": "Scaffolding", + "groups": [ + { "name": "type 0", "start": 0 }, + { "name": "type 1", "start": 10 }, + { "name": "type 2", "start": 24 }, + { "name": "", "start": 36 } + ] + } +] diff --git a/Gui/EditorSettings.cs b/Gui/EditorSettings.cs index d480a633..0bc0b873 100644 --- a/Gui/EditorSettings.cs +++ b/Gui/EditorSettings.cs @@ -43,6 +43,7 @@ public HashSet ObjDataDirectories public string ObjectIndicesFolder { get; set; } = string.Empty; public string DownloadFolder { get; set; } = string.Empty; public string CacheFolder { get; set; } = string.Empty; + public string ConfigFolder { get; set; } = string.Empty; public string LocomotionSteamObjDataFolder { get; set; } = string.Empty; public string LocomotionGoGObjDataFolder { get; set; } = string.Empty; @@ -151,6 +152,11 @@ public bool Validate(ILogger logger) logger.LogWarning("Invalid settings file: ObjData folder \"{ObjDataDirectory}\" does not exist", ObjDataDirectory); return false; } + if (!string.IsNullOrEmpty(ConfigFolder) && !Directory.Exists(ConfigFolder)) + { + logger.LogWarning("Invalid settings file: Config folder \"{ConfigFolder}\" does not exist", ConfigFolder); + return false; + } if (!string.IsNullOrEmpty(CacheFolder) && !Directory.Exists(CacheFolder)) { diff --git a/Gui/Gui.csproj b/Gui/Gui.csproj index 782b79ff..74430e17 100644 --- a/Gui/Gui.csproj +++ b/Gui/Gui.csproj @@ -43,6 +43,12 @@ + + + Gui.ImageTableGroups.json + + + diff --git a/Gui/Models/ObjectEditorContext.cs b/Gui/Models/ObjectEditorContext.cs index e9423f01..a36a539e 100644 --- a/Gui/Models/ObjectEditorContext.cs +++ b/Gui/Models/ObjectEditorContext.cs @@ -6,6 +6,7 @@ using Dat.Types; using Definitions.DTO; using Definitions.ObjectModels; +using Definitions.ObjectModels.Graphics; using Definitions.ObjectModels.Types; using DynamicData; using Index; @@ -13,6 +14,7 @@ using SixLabors.ImageSharp; using System; using System.Collections.Concurrent; +using System.Reflection; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -50,6 +52,7 @@ public class ObjectEditorContext : IDisposable, IAsyncDisposable public const string ApplicationName = "OpenLoco Object Editor"; public const string LoggingFileName = "objectEditor.log"; + public const string ImageTableGroupsConfigFileName = "ImageTableGroups.json"; // stores settings.json, objectEditor.log, etc public static string ProgramDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ApplicationName); @@ -79,6 +82,10 @@ public ObjectEditorContext() Settings.ObjectIndicesFolder = InitialiseDirectory(Settings.ObjectIndicesFolder, "objectIndices"); Settings.CacheFolder = InitialiseDirectory(Settings.CacheFolder, "cache"); Settings.DownloadFolder = InitialiseDirectory(Settings.DownloadFolder, "downloads"); + Settings.ConfigFolder = InitialiseDirectory(Settings.ConfigFolder, "config"); + + EnsureDefaultImageTableGroupConfigExists(Settings.ConfigFolder); + ImageTableGrouper.LoadGroupConfigurationFile(Path.Combine(Settings.ConfigFolder, ImageTableGroupsConfigFileName)); ObjectServiceClient = new(Settings, Logger); ObjectServiceModel = new ObjectServiceModel(ObjectServiceClient, Logger); @@ -155,6 +162,50 @@ string InitialiseDirectory(string folder, string defaultName) return folder; } + void EnsureDefaultImageTableGroupConfigExists(string configFolder) + { + var configFilePath = Path.Combine(configFolder, ImageTableGroupsConfigFileName); + var assemblyPath = Assembly.GetExecutingAssembly().Location; + var assemblyWriteTimeUtc = File.GetLastWriteTimeUtc(assemblyPath); + var fileExists = File.Exists(configFilePath); + + if (fileExists) + { + var existingWriteTimeUtc = File.GetLastWriteTimeUtc(configFilePath); + if (assemblyWriteTimeUtc <= existingWriteTimeUtc) + { + return; + } + } + + try + { + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Gui.ImageTableGroups.json"); + if (stream == null) + { + Logger.LogError("Default image table group configuration resource not found."); + return; + } + + using var reader = new StreamReader(stream); + var text = reader.ReadToEnd(); + File.WriteAllText(configFilePath, text); + + if (fileExists) + { + Logger.LogInformation("Replaced outdated {ImageTableGroupsConfigFileName} from assembly at {ConfigFilePath}", ImageTableGroupsConfigFileName, configFilePath); + } + else + { + Logger.LogInformation("Installed default {ImageTableGroupsConfigFileName} from assembly to {ConfigFilePath}", ImageTableGroupsConfigFileName, configFilePath); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to create default {ImageTableGroupsConfigFileName} at {ConfigFilePath}", ImageTableGroupsConfigFileName, configFilePath); + } + } + public bool TryLoadObject(FileSystemItem filesystemItem, out LocoUIObjectModel? uiLocoFile) { uiLocoFile = null; diff --git a/Gui/ViewModels/Graphics/ImageTableViewModel.cs b/Gui/ViewModels/Graphics/ImageTableViewModel.cs index ad05d6a2..2c0e217c 100644 --- a/Gui/ViewModels/Graphics/ImageTableViewModel.cs +++ b/Gui/ViewModels/Graphics/ImageTableViewModel.cs @@ -4,6 +4,7 @@ using Common.Json; using Definitions.ObjectModels; using Definitions.ObjectModels.Graphics; +using Definitions.ObjectModels.Types; using Microsoft.Extensions.Logging; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -51,6 +52,9 @@ public string ImageCount [Reactive] public ICommand ExportImagesCommand { get; set; } + [Reactive] + public ICommand ReloadImageTableGroupingCommand { get; set; } + [Reactive] public ICommand ReplaceImageCommand { get; set; } [Reactive] @@ -97,14 +101,21 @@ public string ImageCount [Reactive] public ObservableCollection LayeredImages { get; set; } = []; + readonly ObjectType? objectType; + readonly ILocoStruct? objectModel; + readonly string? groupingConfigFilePath; + ImageTable Model { get; init; } - public ImageTableViewModel(ImageTable imageTable, ILogger logger) + public ImageTableViewModel(ImageTable imageTable, ILogger logger, ObjectType? objectType = null, ILocoStruct? objectModel = null, string? groupingConfigFilePath = null) { ArgumentNullException.ThrowIfNull(imageTable); Model = imageTable; Logger = logger; + this.objectType = objectType; + this.objectModel = objectModel; + this.groupingConfigFilePath = groupingConfigFilePath; RecreateViewModelGroupsFromImageTable(Model); // swatches/palettes @@ -160,6 +171,9 @@ public ImageTableViewModel(ImageTable imageTable, ILogger logger) } }); + var canReloadGrouping = objectType.HasValue && !string.IsNullOrEmpty(groupingConfigFilePath); + ReloadImageTableGroupingCommand = ReactiveCommand.CreateFromTask(ReloadImageTableGroupingAsync, Observable.Return(canReloadGrouping)); + TranslateXOffsetAllImagesCommand = ReactiveCommand.Create(amount => { if (short.TryParse(amount, out var result)) @@ -375,6 +389,39 @@ private async Task InsertImageAtAsync(bool insertBefore) } // model stuff + Task ReloadImageTableGroupingAsync() + { + if (!objectType.HasValue || objectModel == null) + { + Logger.LogWarning("Cannot reload image table grouping because object context is unavailable."); + return Task.CompletedTask; + } + + if (string.IsNullOrEmpty(groupingConfigFilePath)) + { + Logger.LogWarning("Cannot reload image table grouping because the grouping configuration file path is not configured."); + return Task.CompletedTask; + } + + ImageTableGrouper.LoadGroupConfigurationFile(groupingConfigFilePath); + + var imageList = Model.GraphicsElements.OrderBy(x => x.ImageTableIndex).ToList(); + + try + { + List groups = [.. ImageTableGrouper.CreateGroupsForExistingImages(objectModel, objectType.Value, imageList)]; + Model.Groups = groups; + RecreateViewModelGroupsFromImageTable(Model); + Logger.LogInformation("Reloaded image table grouping from {ConfigFilePath}", groupingConfigFilePath); + } + catch (Exception ex) + { + Logger.LogError(ex, "Failed to reload image table grouping from {ConfigFilePath}", groupingConfigFilePath); + } + + return Task.CompletedTask; + } + public void RecolourImages(ColourSwatch primary, ColourSwatch secondary) { foreach (var ivm in GroupedImageViewModels.SelectMany(x => x.Images)) diff --git a/Gui/ViewModels/Graphics/ImageViewModel.cs b/Gui/ViewModels/Graphics/ImageViewModel.cs index 1681f336..4b81b9b0 100644 --- a/Gui/ViewModels/Graphics/ImageViewModel.cs +++ b/Gui/ViewModels/Graphics/ImageViewModel.cs @@ -280,7 +280,8 @@ void Dispose(bool disposing) { subscriptions.Dispose(); DisplayedImage?.Dispose(); - Model.Image?.Dispose(); + // The underlying GraphicsElement image is owned by the model and may be shared across view models. + // Do not dispose it here during regrouping. } disposed = true; diff --git a/Gui/ViewModels/Loco/ObjectEditorViewModel.cs b/Gui/ViewModels/Loco/ObjectEditorViewModel.cs index b84df476..acb6879a 100644 --- a/Gui/ViewModels/Loco/ObjectEditorViewModel.cs +++ b/Gui/ViewModels/Loco/ObjectEditorViewModel.cs @@ -283,7 +283,8 @@ public override void Load() if (Model.LocoObject.ImageTable != null) { - AddViewModelToGroup(new ImageTableViewModel(Model.LocoObject.ImageTable, EditorContext.Logger), mediaGroup); + var configFilePath = Path.Combine(EditorContext.Settings.ConfigFolder, ObjectEditorContext.ImageTableGroupsConfigFileName); + AddViewModelToGroup(new ImageTableViewModel(Model.LocoObject.ImageTable, EditorContext.Logger, Model.LocoObject.ObjectType, Model.LocoObject.Object, configFilePath), mediaGroup); var bc = Model.LocoObject.ObjectType == ObjectType.Building ? (Model.LocoObject.Object as IHasBuildingComponents)?.BuildingComponents : null; if (bc != null) diff --git a/Gui/Views/ImageTableView.axaml b/Gui/Views/ImageTableView.axaml index d804cf24..6af42c73 100644 --- a/Gui/Views/ImageTableView.axaml +++ b/Gui/Views/ImageTableView.axaml @@ -50,6 +50,12 @@ x:DataType="vmg:ImageTableViewModel"> +