diff --git a/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs b/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs new file mode 100644 index 00000000..6e1d1067 --- /dev/null +++ b/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Text; +using Xunit; + +namespace Yafc.Model.Serialization.Tests; + +public class SerializationTreeChangeDetection { + private static readonly Dictionary> propertyTree = new() { + [typeof(AutoPlanner)] = new() { + [nameof(AutoPlanner.goals)] = typeof(List), + [nameof(AutoPlanner.done)] = typeof(HashSet), + [nameof(AutoPlanner.roots)] = typeof(HashSet), + }, + [typeof(ModuleFillerParameters)] = new() { + [nameof(ModuleFillerParameters.fillMiners)] = typeof(bool), + [nameof(ModuleFillerParameters.autoFillPayback)] = typeof(float), + [nameof(ModuleFillerParameters.fillerModule)] = typeof(ObjectWithQuality), + [nameof(ModuleFillerParameters.beacon)] = typeof(ObjectWithQuality), + [nameof(ModuleFillerParameters.beaconModule)] = typeof(ObjectWithQuality), + [nameof(ModuleFillerParameters.beaconsPerBuilding)] = typeof(int), + [nameof(ModuleFillerParameters.overrideCrafterBeacons)] = typeof(OverrideCrafterBeacons), + }, + [typeof(ProductionSummaryGroup)] = new() { + [nameof(ProductionSummaryGroup.elements)] = typeof(List), + [nameof(ProductionSummaryGroup.expanded)] = typeof(bool), + [nameof(ProductionSummaryGroup.name)] = typeof(string), + }, + [typeof(ProductionSummaryEntry)] = new() { + [nameof(ProductionSummaryEntry.multiplier)] = typeof(float), + [nameof(ProductionSummaryEntry.page)] = typeof(PageReference), + [nameof(ProductionSummaryEntry.subgroup)] = typeof(ProductionSummaryGroup), + }, + [typeof(ProductionSummaryColumn)] = new() { + [nameof(ProductionSummaryColumn.goods)] = typeof(ObjectWithQuality), + }, + [typeof(ProductionSummary)] = new() { + [nameof(ProductionSummary.group)] = typeof(ProductionSummaryGroup), + [nameof(ProductionSummary.columns)] = typeof(List), + }, + [typeof(ProductionTable)] = new() { + [nameof(ProductionTable.expanded)] = typeof(bool), + [nameof(ProductionTable.links)] = typeof(List), + [nameof(ProductionTable.recipes)] = typeof(List), + [nameof(ProductionTable.modules)] = typeof(ModuleFillerParameters), + }, + [typeof(RecipeRowCustomModule)] = new() { + [nameof(RecipeRowCustomModule.module)] = typeof(ObjectWithQuality), + [nameof(RecipeRowCustomModule.fixedCount)] = typeof(int), + }, + [typeof(ModuleTemplate)] = new() { + [nameof(ModuleTemplate.beacon)] = typeof(ObjectWithQuality), + [nameof(ModuleTemplate.list)] = typeof(ReadOnlyCollection), + [nameof(ModuleTemplate.beaconList)] = typeof(ReadOnlyCollection), + }, + [typeof(RecipeRow)] = new() { + [nameof(RecipeRow.recipe)] = typeof(ObjectWithQuality), + [nameof(RecipeRow.entity)] = typeof(ObjectWithQuality), + [nameof(RecipeRow.fuel)] = typeof(ObjectWithQuality), + [nameof(RecipeRow.fixedBuildings)] = typeof(float), + [nameof(RecipeRow.fixedFuel)] = typeof(bool), + [nameof(RecipeRow.fixedIngredient)] = typeof(ObjectWithQuality), + [nameof(RecipeRow.fixedProduct)] = typeof(ObjectWithQuality), + [nameof(RecipeRow.builtBuildings)] = typeof(int?), + [nameof(RecipeRow.showTotalIO)] = typeof(bool), + [nameof(RecipeRow.enabled)] = typeof(bool), + [nameof(RecipeRow.tag)] = typeof(int), + [nameof(RecipeRow.modules)] = typeof(ModuleTemplate), + [nameof(RecipeRow.subgroup)] = typeof(ProductionTable), + [nameof(RecipeRow.variants)] = typeof(HashSet), + }, + [typeof(ProductionLink)] = new() { + [nameof(ProductionLink.goods)] = typeof(ObjectWithQuality), + [nameof(ProductionLink.amount)] = typeof(float), + [nameof(ProductionLink.algorithm)] = typeof(LinkAlgorithm), + }, + [typeof(Project)] = new() { + [nameof(Project.settings)] = typeof(ProjectSettings), + [nameof(Project.preferences)] = typeof(ProjectPreferences), + [nameof(Project.sharedModuleTemplates)] = typeof(List), + [nameof(Project.yafcVersion)] = typeof(string), + [nameof(Project.pages)] = typeof(List), + [nameof(Project.displayPages)] = typeof(List), + }, + [typeof(ProjectSettings)] = new() { + [nameof(ProjectSettings.milestones)] = typeof(List), + [nameof(ProjectSettings.itemFlags)] = typeof(SortedList), + [nameof(ProjectSettings.miningProductivity)] = typeof(float), + [nameof(ProjectSettings.researchSpeedBonus)] = typeof(float), + [nameof(ProjectSettings.researchProductivity)] = typeof(float), + [nameof(ProjectSettings.productivityTechnologyLevels)] = typeof(Dictionary), + [nameof(ProjectSettings.reactorSizeX)] = typeof(int), + [nameof(ProjectSettings.reactorSizeY)] = typeof(int), + [nameof(ProjectSettings.PollutionCostModifier)] = typeof(float), + [nameof(ProjectSettings.spoilingRate)] = typeof(float), + }, + [typeof(ProjectPreferences)] = new() { + [nameof(ProjectPreferences.time)] = typeof(int), + [nameof(ProjectPreferences.itemUnit)] = typeof(float), + [nameof(ProjectPreferences.fluidUnit)] = typeof(float), + [nameof(ProjectPreferences.defaultBelt)] = typeof(EntityBelt), + [nameof(ProjectPreferences.defaultInserter)] = typeof(EntityInserter), + [nameof(ProjectPreferences.inserterCapacity)] = typeof(int), + [nameof(ProjectPreferences.sourceResources)] = typeof(HashSet), + [nameof(ProjectPreferences.favorites)] = typeof(HashSet), + [nameof(ProjectPreferences.targetTechnology)] = typeof(Technology), + [nameof(ProjectPreferences.iconScale)] = typeof(float), + [nameof(ProjectPreferences.maxMilestonesPerTooltipLine)] = typeof(int), + [nameof(ProjectPreferences.showMilestoneOnInaccessible)] = typeof(bool), + }, + [typeof(ProjectModuleTemplate)] = new() { + [nameof(ProjectModuleTemplate.template)] = typeof(ModuleTemplate), + [nameof(ProjectModuleTemplate.icon)] = typeof(FactorioObject), + [nameof(ProjectModuleTemplate.name)] = typeof(string), + [nameof(ProjectModuleTemplate.filterEntities)] = typeof(List), + }, + [typeof(ProjectPage)] = new() { + [nameof(ProjectPage.icon)] = typeof(FactorioObject), + [nameof(ProjectPage.name)] = typeof(string), + [nameof(ProjectPage.guid)] = typeof(Guid), + [nameof(ProjectPage.contentType)] = typeof(Type), + [nameof(ProjectPage.content)] = typeof(ProjectPageContents), + }, + [typeof(Summary)] = new() { + [nameof(Summary.showOnlyIssues)] = typeof(bool), + }, + [typeof(AutoPlannerGoal)] = new() { + [nameof(AutoPlannerGoal.item)] = typeof(Goods), + [nameof(AutoPlannerGoal.amount)] = typeof(float), + }, + [typeof(ObjectWithQuality)] = new() { + [nameof(ObjectWithQuality.target)] = typeof(Module), + [nameof(ObjectWithQuality.quality)] = typeof(Quality), + }, + [typeof(ObjectWithQuality)] = new() { + [nameof(ObjectWithQuality.target)] = typeof(EntityBeacon), + [nameof(ObjectWithQuality.quality)] = typeof(Quality), + }, + [typeof(BeaconOverrideConfiguration)] = new() { + [nameof(BeaconOverrideConfiguration.beacon)] = typeof(ObjectWithQuality), + [nameof(BeaconOverrideConfiguration.beaconCount)] = typeof(int), + [nameof(BeaconOverrideConfiguration.beaconModule)] = typeof(ObjectWithQuality), + }, + [typeof(ObjectWithQuality)] = new() { + [nameof(ObjectWithQuality.target)] = typeof(Goods), + [nameof(ObjectWithQuality.quality)] = typeof(Quality), + }, + [typeof(ObjectWithQuality)] = new() { + [nameof(ObjectWithQuality.target)] = typeof(RecipeOrTechnology), + [nameof(ObjectWithQuality.quality)] = typeof(Quality), + }, + [typeof(ObjectWithQuality)] = new() { + [nameof(ObjectWithQuality.target)] = typeof(EntityCrafter), + [nameof(ObjectWithQuality.quality)] = typeof(Quality), + }, + }; + + private readonly HashSet queuedTypes = [.. AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => typeof(ModelObject).IsAssignableFrom(t) && !t.IsAbstract)]; + private readonly Queue queue; + + public SerializationTreeChangeDetection() => queue = new(queuedTypes); + + [Fact] + // Walk all serialized types (starting with all concrete ModelObjects), and check which types are serialized and the types of their + // properties. Changes to this list may result in save/load issues, so require an extra step (modifying the above dictionary initializer) + // to ensure those changes are intentional. + public void TreeHasNotChanged() { + while (queue.TryDequeue(out Type type)) { + Assert.True(propertyTree.Remove(type, out var expectedProperties), $"Serializing new type {MakeTypeName(type)}. Add `[typeof({MakeTypeName(type)})] = new() {{ /*properties*/ }},` to the propertyTree initializer."); + + var constructorWritable = SerializationTypeValidation.FindConstructor(type).GetParameters().Select(p => p.Name); + if (typeof(ModelObject).IsAssignableFrom(type)) { + // Skip the parent/owner parameter + constructorWritable = constructorWritable.Skip(1); + } + HashSet constructorParameters = [.. constructorWritable]; + + foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { + if (property.GetCustomAttribute() != null || property.GetCustomAttribute() != null) { + continue; + } + + Type propertyType = property.PropertyType; + + if (property.GetSetMethod() != null || constructorParameters.Contains(property.Name)) { + // Writable properties are always serialized + AssertPropertyType(type, expectedProperties, property, propertyType); + QueueSerializedType(propertyType); + } + else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(ReadOnlyCollection<>) + && ValueSerializer.IsValueSerializerSupported(propertyType.GenericTypeArguments[0])) { + + // ReadOnlyCollection is serialized as T + AssertPropertyType(type, expectedProperties, property, propertyType); + QueueSerializedType(propertyType.GenericTypeArguments[0]); + } + else if (propertyType.IsAssignableTo(typeof(ModelObject))) { + // Read-only model objects are serialized in-place. + AssertPropertyType(type, expectedProperties, property, propertyType); + QueueSerializedType(propertyType); // Only for consistency; the queue is initialized with all ModelObjects. + } + else { + var interfaces = propertyType.GetInterfaces().Where(i => i.IsGenericType).DistinctBy(i => i.GetGenericTypeDefinition()).ToList(); + if (interfaces.FirstOrDefault(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)) is Type iDictionary) { + if (ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[0]) + && ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[1])) { + + var types = iDictionary.GenericTypeArguments; + AssertPropertyType(type, expectedProperties, property, propertyType); + QueueSerializedType(types[0]); + QueueSerializedType(types[1]); + } + } + else if (interfaces.FirstOrDefault(i => i.GetGenericTypeDefinition() == typeof(ICollection<>)) is Type iCollection) { + if (ValueSerializer.IsValueSerializerSupported(iCollection.GenericTypeArguments[0])) { + var types = iCollection.GenericTypeArguments; + AssertPropertyType(type, expectedProperties, property, propertyType); + QueueSerializedType(types[0]); + } + } + } + } + + if (expectedProperties.Keys.FirstOrDefault() is string missingProperty) { + Assert.Fail($"Expected to serialize property {MakeTypeName(type)}.{missingProperty}. Remove its entry from the propertyTree initializer if it is obsolete or no longer supported."); + } + } + + if (propertyTree.Keys.FirstOrDefault() is Type missingType) { + Assert.Fail($"Expected to serialize type {MakeTypeName(missingType)}. Remove its entry from the propertyTree initializer if it is obsolete or no longer supported."); + } + } + + // Not explictly called, but you can call this from the debugger to generate the initializer as a string. + public string BuildPropertyTree() { + StringBuilder result = new(" private static readonly Dictionary> propertyTree = new() {\r\n"); + HashSet queuedTypes = [.. this.queuedTypes]; + Queue queue = new(queuedTypes); + while (queue.TryDequeue(out Type type)) { + result.AppendLine($" [typeof({MakeTypeName(type)})] = new() {{"); + foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { + if (property.GetCustomAttribute() != null || property.GetCustomAttribute() != null) { + continue; + } + + var constructorWritable = SerializationTypeValidation.FindConstructor(type).GetParameters().Select(p => p.Name); + if (typeof(ModelObject).IsAssignableFrom(type)) { + // Skip the parent/owner parameter + constructorWritable = constructorWritable.Skip(1); + } + HashSet constructorParameters = [.. constructorWritable]; + + Type propertyType = property.PropertyType; + + if (property.GetSetMethod() != null || constructorParameters.Contains(property.Name)) { + // Writable properties are always serialized + addPropertyType(type, property, propertyType); + queueSerializedType(propertyType); + } + else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(ReadOnlyCollection<>) + && ValueSerializer.IsValueSerializerSupported(propertyType.GenericTypeArguments[0])) { + + // ReadOnlyCollection is serialized as T + addPropertyType(type, property, propertyType); + queueSerializedType(propertyType.GenericTypeArguments[0]); + } + else if (propertyType.IsAssignableTo(typeof(ModelObject))) { + // Read-only model objects are serialized in-place + addPropertyType(type, property, propertyType); + queueSerializedType(propertyType); + } + else { + var interfaces = propertyType.GetInterfaces().Where(i => i.IsGenericType).DistinctBy(i => i.GetGenericTypeDefinition()).ToList(); + if (interfaces.FirstOrDefault(i => i.GetGenericTypeDefinition() == typeof(IDictionary<,>)) is Type iDictionary) { + if (ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[0]) + && ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[1])) { + + var types = iDictionary.GenericTypeArguments; + addPropertyType(type, property, propertyType); + queueSerializedType(types[0]); + queueSerializedType(types[1]); + } + } + else if (interfaces.FirstOrDefault(i => i.GetGenericTypeDefinition() == typeof(ICollection<>)) is Type iCollection) { + if (ValueSerializer.IsValueSerializerSupported(iCollection.GenericTypeArguments[0])) { + var types = iCollection.GenericTypeArguments; + addPropertyType(type, property, propertyType); + queueSerializedType(types[0]); + } + } + } + } + result.AppendLine(" },"); + } + + return result.AppendLine(" };").ToString(); + + void addPropertyType(Type type, PropertyInfo property, Type propertyType) + => result.AppendLine($" [nameof({MakeTypeName(type)}.{property.Name})] = typeof({MakeTypeName(propertyType)}),"); + + void queueSerializedType(Type serializationType) { + if ((serializationType.GetCustomAttribute() != null && serializationType.FullName.StartsWith("Yafc.")) || serializationType.IsAssignableTo(typeof(ModelObject))) { + if (!serializationType.IsAbstract && queuedTypes.Add(serializationType)) { + queue.Enqueue(serializationType); + } + } + } + } + + /// + /// Assert that is expected to be serialized and has the expected type. + /// + private static void AssertPropertyType(Type type, Dictionary expectedProperties, PropertyInfo property, Type propertyType) { + Assert.True(expectedProperties.Remove(property.Name, out Type expectedPropertyType), + $"Serializing a new property '{MakeTypeName(type)}.{property.Name}'. If this is intentional, add `[nameof({MakeTypeName(type)}.{property.Name})] = typeof({MakeTypeName(property.PropertyType)}),` to the propertyTree initializer, under [typeof({MakeTypeName(type)})]."); + Assert.True(expectedPropertyType == propertyType, + $"The type of '{MakeTypeName(type)}.{property.Name}' has changed from {MakeTypeName(expectedPropertyType)} to {MakeTypeName(propertyType)}. If this is intentional, update the property type in the propertyTree initializer, under [typeof({MakeTypeName(type)})]."); + } + + /// + /// If appropriate, queue for inspection of its property types. + /// Native and native-like types do not get inspected, and each type is inspected at most once. + /// + private void QueueSerializedType(Type serializationType) { + if ((serializationType.GetCustomAttribute() != null && serializationType.FullName.StartsWith("Yafc.")) + || (serializationType.IsAssignableTo(typeof(ModelObject)) && !serializationType.IsAbstract)) { + + if (queuedTypes.Add(serializationType)) { + queue.Enqueue(serializationType); + } + } + } + + private static string MakeTypeName(Type type) => SerializationTypeValidation.MakeTypeName(type); +} diff --git a/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs new file mode 100644 index 00000000..5f9cbfab --- /dev/null +++ b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using Xunit; + +namespace Yafc.Model.Serialization.Tests; + +public class SerializationTypeValidation { + [Fact] + // Ensure that all concrete types derived from ModelObject obey the serialization rules. + public void ModelObjects_AreSerializable() { + var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => typeof(ModelObject).IsAssignableFrom(t) && !t.IsAbstract); + + foreach (Type type in types) { + ConstructorInfo constructor = FindConstructor(type); + + PropertyInfo ownerProperty = type.GetProperty("owner"); + if (ownerProperty != null) { + // If derived from ModelObject (as tested by "There's an 'owner' property"), the first constructor parameter must be T. + Assert.True(constructor.GetParameters().Length > 0, $"The first constructor parameter for type {MakeTypeName(type)} should be the parent object."); + Type baseType = typeof(ModelObject<>).MakeGenericType(ownerProperty.PropertyType); + Assert.True(baseType.IsAssignableFrom(type), $"The first constructor parameter for type {MakeTypeName(type)} is not the parent type."); + } + + // Cheating a bit here: Project is the only ModelObject that is not a ModelObject, and its constructor has no parameters. + // So we're just skipping a parameter that doesn't exist. + AssertConstructorParameters(type, constructor.GetParameters().Skip(1)); + AssertSettableProperties(type); + AssertDictionaryKeys(type); + } + } + + [Fact] + // Ensure that all [Serializable] types in the Yafc namespace obey the serialization rules, + // except compiler-generated types and those in Yafc.Blueprints. + public void Serializables_AreSerializable() { + var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => t.GetCustomAttribute() != null && t.FullName.StartsWith("Yafc.") && !t.FullName.StartsWith("Yafc.Blueprints")); + + foreach (Type type in types.Where(type => type.GetCustomAttribute() == null)) { + ConstructorInfo constructor = FindConstructor(type); + + AssertConstructorParameters(type, constructor.GetParameters()); + AssertSettableProperties(type); + AssertDictionaryKeys(type); + } + } + + internal static ConstructorInfo FindConstructor(Type type) { + BindingFlags flags = BindingFlags.Instance; + if (type.GetCustomAttribute() != null) { + flags |= BindingFlags.NonPublic; + } + else { + flags |= BindingFlags.Public; + } + + // The constructor (public or non-public, depending on the attribute) must exist + ConstructorInfo constructor = type.GetConstructors(flags).FirstOrDefault(); + Assert.True(constructor != null, $"Could not find the constructor for type {MakeTypeName(type)}."); + return constructor; + } + + + private static void AssertConstructorParameters(Type type, IEnumerable parameters) { + foreach (ParameterInfo parameter in parameters) { + // Constructor parameters must be IsValueSerializerSupported + Assert.True(ValueSerializer.IsValueSerializerSupported(parameter.ParameterType), + $"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' should be a supported value type."); + + // and must have a matching property + PropertyInfo property = type.GetProperty(parameter.Name); + Assert.True(property != null, $"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' does not have a matching property."); + Assert.True(parameter.ParameterType == property.PropertyType, + $"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' does not have the same type as its property."); + } + } + + private static void AssertSettableProperties(Type type) { + foreach (PropertyInfo property in type.GetProperties().Where(p => p.GetSetMethod() != null)) { + if (property.GetCustomAttribute() == null) { + // Properties with a public setter must be IsValueSerializerSupported + Assert.True(ValueSerializer.IsValueSerializerSupported(property.PropertyType), + $"The type of property {MakeTypeName(type)}.{property.Name} should be a supported value type."); + } + } + } + + private void AssertDictionaryKeys(Type type) { + foreach (PropertyInfo property in type.GetProperties().Where(p => p.GetSetMethod() == null)) { + if (property.GetCustomAttribute() == null) { + Type iDictionary = property.PropertyType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + + if (iDictionary != null) { + if (ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[0]) + && ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[1])) { + + object serializer = typeof(ValueSerializer<>).MakeGenericType(iDictionary.GenericTypeArguments[0]).GetField("Default").GetValue(null); + MethodInfo getJsonProperty = serializer.GetType().GetMethod(nameof(ValueSerializer.GetJsonProperty)); + // Dictionary keys must be serialized by an overridden ValueSerializer.GetJsonProperty() method. + Assert.True(getJsonProperty != getJsonProperty.GetBaseDefinition(), + $"In {MakeTypeName(type)}.{property.Name}, the dictionary keys are of an unsupported type."); + } + } + } + } + } + + internal static string MakeTypeName(Type type) { + if (type.IsGenericType) { + if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) { + return MakeTypeName(type.GetGenericArguments()[0]) + '?'; + } + StringBuilder result = new(type.Name.Split('`')[0]); + result.Append('<').AppendJoin(", ", type.GenericTypeArguments.Select(MakeTypeName)).Append('>'); + return result.ToString(); + } + + if (type == typeof(bool)) { return "bool"; } + if (type == typeof(byte)) { return "byte"; } + if (type == typeof(sbyte)) { return "sbyte"; } + if (type == typeof(char)) { return "char"; } + if (type == typeof(short)) { return "short"; } + if (type == typeof(ushort)) { return "ushort"; } + if (type == typeof(int)) { return "int"; } + if (type == typeof(uint)) { return "uint"; } + if (type == typeof(long)) { return "long"; } + if (type == typeof(ulong)) { return "ulong"; } + if (type == typeof(nint)) { return "nint"; } + if (type == typeof(nuint)) { return "nuint"; } + if (type == typeof(float)) { return "float"; } + if (type == typeof(double)) { return "double"; } + if (type == typeof(decimal)) { return "decimal"; } + if (type == typeof(string)) { return "string"; } + if (type == typeof(object)) { return "object"; } + + return type.Name; + } +} diff --git a/Yafc.Model/Model/ProductionSummary.cs b/Yafc.Model/Model/ProductionSummary.cs index 1650c91c..a677788c 100644 --- a/Yafc.Model/Model/ProductionSummary.cs +++ b/Yafc.Model/Model/ProductionSummary.cs @@ -155,8 +155,8 @@ public void SetMultiplier(float newMultiplier) { } } -public class ProductionSummaryColumn(ProductionSummary owner, IObjectWithQuality goods) : ModelObject(owner) { - public IObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Object does not exist"); +public class ProductionSummaryColumn(ProductionSummary owner, ObjectWithQuality goods) : ModelObject(owner) { + public ObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Object does not exist"); } public class ProductionSummary : ProjectPageContents, IComparer<(IObjectWithQuality goods, float amount)> { @@ -169,7 +169,7 @@ public class ProductionSummary : ProjectPageContents, IComparer<(IObjectWithQual [SkipSerialization] public HashSet> columnsExist { get; } = []; public override void InitNew() { - columns.Add(new ProductionSummaryColumn(this, Database.electricity)); + columns.Add(new ProductionSummaryColumn(this, new(Database.electricity.target, Quality.Normal))); base.InitNew(); } diff --git a/Yafc.Model/Model/ProductionTableContent.cs b/Yafc.Model/Model/ProductionTableContent.cs index f15ddf61..e5c6cc44 100644 --- a/Yafc.Model/Model/ProductionTableContent.cs +++ b/Yafc.Model/Model/ProductionTableContent.cs @@ -78,7 +78,7 @@ public class RecipeRowCustomModule(ModuleTemplate owner, ObjectWithQuality. /// /// Immutable. To modify, call , modify the builder, and call . -[Serializable, DeserializeWithNonPublicConstructor] +[DeserializeWithNonPublicConstructor] public class ModuleTemplate : ModelObject { /// /// The beacon to use, if any, for the associated . diff --git a/Yafc.Model/Yafc.Model.csproj b/Yafc.Model/Yafc.Model.csproj index c92c8a84..2edc61a2 100644 --- a/Yafc.Model/Yafc.Model.csproj +++ b/Yafc.Model/Yafc.Model.csproj @@ -7,10 +7,8 @@ + - - - diff --git a/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs b/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs index d8aaa844..d45ee88d 100644 --- a/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs +++ b/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs @@ -266,7 +266,7 @@ private void AddOrRemoveColumn(IObjectWithQuality goods) { } if (!found) { - model.columns.Add(new ProductionSummaryColumn(model, goods)); + model.columns.Add(new ProductionSummaryColumn(model, new(goods.target, goods.quality))); } } diff --git a/changelog.txt b/changelog.txt index 477d33e3..ae5a34c6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,8 @@ Version: Date: Features: - Add related recipes to the Link Summary screen. + Fixes: + - (regression) Legacy summary pages could not be saved or loaded. ---------------------------------------------------------------------------------------------------------------------- Version: 2.8.1 Date: February 20th 2025