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">
+