From 7beee78c171e6fc18c7a8df390fbb2bf4a83bfd7 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 21 Mar 2025 08:36:53 -0400 Subject: [PATCH 01/54] change(test): Replace some [Fact] foreach tests with [Theory]s. This increases the number of errors that can be reported simultaneously. --- .../SerializationTypeValidation.cs | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs index 5f9cbfab..1df18d01 100644 --- a/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs +++ b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs @@ -9,47 +9,46 @@ namespace Yafc.Model.Serialization.Tests; public class SerializationTypeValidation { - [Fact] + [Theory] + [MemberData(nameof(ModelObjectTypes))] // 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); + public void ModelObjects_AreSerializable(Type modelObjectType) { + ConstructorInfo constructor = FindConstructor(modelObjectType); + + PropertyInfo ownerProperty = modelObjectType.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(modelObjectType)} should be the parent object."); + Type baseType = typeof(ModelObject<>).MakeGenericType(ownerProperty.PropertyType); + Assert.True(baseType.IsAssignableFrom(modelObjectType), $"The first constructor parameter for type {MakeTypeName(modelObjectType)} 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(modelObjectType, constructor.GetParameters().Skip(1)); + AssertSettableProperties(modelObjectType); + AssertDictionaryKeys(modelObjectType); } - [Fact] + public static TheoryData ModelObjectTypes => [.. AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => typeof(ModelObject).IsAssignableFrom(t) && !t.IsAbstract)]; + + [Theory] + [MemberData(nameof(SerializableTypes))] // 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")); + public void Serializables_AreSerializable(Type serializableType) { + ConstructorInfo constructor = FindConstructor(serializableType); - foreach (Type type in types.Where(type => type.GetCustomAttribute() == null)) { - ConstructorInfo constructor = FindConstructor(type); - - AssertConstructorParameters(type, constructor.GetParameters()); - AssertSettableProperties(type); - AssertDictionaryKeys(type); - } + AssertConstructorParameters(serializableType, constructor.GetParameters()); + AssertSettableProperties(serializableType); + AssertDictionaryKeys(serializableType); } + public static TheoryData SerializableTypes => [.. AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => t.FullName.StartsWith("Yafc.") && !t.FullName.StartsWith("Yafc.Blueprints") + && t.GetCustomAttribute() != null && t.GetCustomAttribute() == null)]; + internal static ConstructorInfo FindConstructor(Type type) { BindingFlags flags = BindingFlags.Instance; if (type.GetCustomAttribute() != null) { From ce16270025fa7a9d854f5f099a354aae9bbed132 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 21 Mar 2025 08:39:00 -0400 Subject: [PATCH 02/54] change(test): Replace a repeated [Fact] with a single [Theory]. The new Theory will also automatically test any new ProductionTableContent classes. --- .../Model/ProductionTableTests.cs | 69 ++++++------------- 1 file changed, 21 insertions(+), 48 deletions(-) diff --git a/Yafc.Model.Tests/Model/ProductionTableTests.cs b/Yafc.Model.Tests/Model/ProductionTableTests.cs index fce661b7..a3e7f0da 100644 --- a/Yafc.Model.Tests/Model/ProductionTableTests.cs +++ b/Yafc.Model.Tests/Model/ProductionTableTests.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Reflection; using Xunit; @@ -7,22 +8,6 @@ namespace Yafc.Model.Tests.Model; [Collection("LuaDependentTests")] public class ProductionTableTests { - [Fact] - public void ProductionTableTest_CanSaveAndLoadWithEmptyPage() { - Project project = LuaDependentTestHelper.GetProjectForLua("Yafc.Model.Tests.Model.ProductionTableContentTests.lua"); - - ProjectPage page = new(project, typeof(ProductionTable)); - project.pages.Add(page); - - ErrorCollector collector = new(); - using MemoryStream stream = new(); - project.Save(stream); - Project newProject = Project.Read(stream.ToArray(), collector); - - Assert.Equal(ErrorSeverity.None, collector.severity); - Assert.Equal(project.pages.Select(s => s.guid), newProject.pages.Select(p => p.guid)); - } - [Fact] public void ProductionTableTest_CanSaveAndLoadWithRecipe() { Project project = LuaDependentTestHelper.GetProjectForLua("Yafc.Model.Tests.Model.ProductionTableContentTests.lua"); @@ -83,26 +68,36 @@ public void ProductionTableTest_CanSaveAndLoadWithNonemptySubtable() { } [Fact] - public void ProductionTableTest_CanSaveAndLoadWithProductionSummary() { + public void ProductionTableTest_CanLoadWithUnexpectedObject() { Project project = LuaDependentTestHelper.GetProjectForLua("Yafc.Model.Tests.Model.ProductionTableContentTests.lua"); - ProjectPage page = new(project, typeof(ProductionSummary)); + ProjectPage page = new(project, typeof(ProductionTable)); project.pages.Add(page); + ProductionTable table = (ProductionTable)page.content; + table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + RecipeRow row = table.GetAllRecipes().Single(); + row.subgroup = new ProductionTable(row); + + // Force the subgroup to have a value in modules, which is not present in a normal project. + typeof(ProductionTable).GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(f => f.FieldType == typeof(ModuleFillerParameters)) + .SetValue(row.subgroup, new ModuleFillerParameters(row.subgroup)); ErrorCollector collector = new(); using MemoryStream stream = new(); project.Save(stream); - Project newProject = Project.Read(stream.ToArray(), collector); + Project newProject = Project.Read(stream.ToArray(), collector); // This reader is expected to skip the unexpected value with a MinorDataLoss warning. - Assert.Equal(ErrorSeverity.None, collector.severity); + Assert.Equal(ErrorSeverity.MinorDataLoss, collector.severity); Assert.Equal(project.pages.Select(s => s.guid), newProject.pages.Select(p => p.guid)); } - [Fact] - public void ProductionTableTest_CanSaveAndLoadWithSummary() { + [Theory] + [MemberData(nameof(ProjectPageContentTypes))] + public void ProductionTableTest_CanSaveAndLoadWithEachPageContentType(Type contentType) { Project project = LuaDependentTestHelper.GetProjectForLua("Yafc.Model.Tests.Model.ProductionTableContentTests.lua"); - ProjectPage page = new(project, typeof(Summary)); + ProjectPage page = new(project, contentType); project.pages.Add(page); ErrorCollector collector = new(); @@ -114,28 +109,6 @@ public void ProductionTableTest_CanSaveAndLoadWithSummary() { Assert.Equal(project.pages.Select(s => s.guid), newProject.pages.Select(p => p.guid)); } - [Fact] - public void ProductionTableTest_CanLoadWithUnexpectedObject() { - Project project = LuaDependentTestHelper.GetProjectForLua("Yafc.Model.Tests.Model.ProductionTableContentTests.lua"); - - ProjectPage page = new(project, typeof(ProductionTable)); - project.pages.Add(page); - ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); - RecipeRow row = table.GetAllRecipes().Single(); - row.subgroup = new ProductionTable(row); - - // Force the subgroup to have a value in modules, which is not present in a normal project. - typeof(ProductionTable).GetFields(BindingFlags.NonPublic | BindingFlags.Instance) - .Single(f => f.FieldType == typeof(ModuleFillerParameters)) - .SetValue(row.subgroup, new ModuleFillerParameters(row.subgroup)); - - ErrorCollector collector = new(); - using MemoryStream stream = new(); - project.Save(stream); - Project newProject = Project.Read(stream.ToArray(), collector); // This reader is expected to skip the unexpected value with a MinorDataLoss warning. - - Assert.Equal(ErrorSeverity.MinorDataLoss, collector.severity); - Assert.Equal(project.pages.Select(s => s.guid), newProject.pages.Select(p => p.guid)); - } + public static TheoryData ProjectPageContentTypes => [.. AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => typeof(ProjectPageContents).IsAssignableFrom(t) && !t.IsAbstract)]; } From 10d8f62b9613ba3ec97c4f689b54acd3f50727b0 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 24 Feb 2025 02:49:59 -0500 Subject: [PATCH 03/54] change: Provide reference equality for all `IObjectWithQuality`s. Serialization is completely broken here. --- Docs/CodeStyle.md | 19 --- .../Model/ProductionTableContentTests.cs | 36 ++--- .../Model/ProductionTableTests.cs | 10 +- .../Model/RecipeParametersTests.cs | 4 +- .../Model/SelectableVariantsTests.cs | 6 +- .../SerializationTreeChangeDetection.cs | 48 ++----- Yafc.Model.Tests/TestHelpers.cs | 2 +- Yafc.Model/Data/DataClasses.cs | 124 ++++++++++-------- Yafc.Model/Data/Database.cs | 14 +- Yafc.Model/Model/ModuleFillerParameters.cs | 32 ++--- Yafc.Model/Model/ProductionSummary.cs | 6 +- .../Model/ProductionTable.GenuineRecipe.cs | 4 +- .../Model/ProductionTable.ImplicitLink.cs | 6 +- .../ProductionTable.ScienceDecomposition.cs | 4 +- Yafc.Model/Model/ProductionTable.cs | 14 +- Yafc.Model/Model/ProductionTableContent.cs | 79 +++++------ Yafc.Model/Model/QualityExtensions.cs | 63 ++++----- Yafc.Model/Model/RecipeParameters.cs | 10 +- Yafc.Parser/Data/FactorioDataDeserializer.cs | 1 + .../Data/FactorioDataDeserializer_Context.cs | 14 +- Yafc/Widgets/ImmediateWidgets.cs | 8 +- Yafc/Widgets/ObjectTooltip.cs | 2 +- Yafc/Windows/ProjectPageSettingsPanel.cs | 15 ++- Yafc/Windows/SelectMultiObjectPanel.cs | 2 +- Yafc/Windows/SelectObjectPanel.cs | 4 +- Yafc/Windows/SelectSingleObjectPanel.cs | 4 +- Yafc/Windows/ShoppingListScreen.cs | 12 +- .../ProductionSummaryView.cs | 8 +- .../ModuleCustomizationScreen.cs | 8 +- .../ModuleFillerParametersScreen.cs | 6 +- .../ProductionLinkSummaryScreen.cs | 8 +- .../ProductionTable/ProductionTableView.cs | 42 +++--- changelog.txt | 5 + 33 files changed, 300 insertions(+), 320 deletions(-) diff --git a/Docs/CodeStyle.md b/Docs/CodeStyle.md index 789c729d..dd4633cf 100644 --- a/Docs/CodeStyle.md +++ b/Docs/CodeStyle.md @@ -74,22 +74,3 @@ Most of the operators like `.` `+` `&&` go to the next line. The notable operators that stay on the same line are `=>`, `=> {`, and `,`. The wrapping of arguments in constructors and method-definitions is up to you. - -# Quality Implementation -Despite several attempts, the implementation of quality levels is unpleasantly twisted. -Here are some guidelines to keep handling of quality levels from causing additional problems: -* **Serialization** - * All serialized values (public properties of `ModelObject`s and `[Serializable]` classes) must be a suitable concrete type. - (That is, `ObjectWithQuality`, not `IObjectWithQuality`) -* **Equality** - * Where `==` operators are expected/used, prefer the concrete type. - The `==` operator will silently revert to reference equality if both sides of are the interface type. - * On the other hand, the `==` operator will fail to compile if the two sides are distinct concrete types. - Use `.As()` to convert one side to the interface type. - * Types that call `Object.Equals`, such as `Dictionary` and `HashSet`, will behave correctly on both the interface and concrete types. - The interface type may be more convenient for things like dictionary keys or hashset values. -* **Conversion** - * There is a conversion from `(T?, Quality)` to `ObjectWithQuality?`, where a null input will return a null result. - If the input is definitely not null, use the constructor instead. - C# prohibits conversions to or from interface types. - * The interface types are covariant; an `IObjectWithQuality` may be used as an `IObjectWithQuality` in the same way that an `Item` may be used as a `Goods`. diff --git a/Yafc.Model.Tests/Model/ProductionTableContentTests.cs b/Yafc.Model.Tests/Model/ProductionTableContentTests.cs index f19486fe..1f07e99d 100644 --- a/Yafc.Model.Tests/Model/ProductionTableContentTests.cs +++ b/Yafc.Model.Tests/Model/ProductionTableContentTests.cs @@ -15,11 +15,11 @@ public void ChangeFuelEntityModules_ShouldPreserveFixedAmount() { ProjectPage page = new ProjectPage(project, typeof(ProductionTable)); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); - table.modules.beacon = new(Database.allBeacons.Single(), Quality.Normal); - table.modules.beaconModule = new(Database.allModules.Single(m => m.name == "speed-module"), Quality.Normal); + table.modules.beacon = Database.allBeacons.Single().With(Quality.Normal); + table.modules.beaconModule = Database.allModules.Single(m => m.name == "speed-module").With(Quality.Normal); table.modules.beaconsPerBuilding = 2; table.modules.autoFillPayback = MathF.Sqrt(float.MaxValue); @@ -29,16 +29,16 @@ public void ChangeFuelEntityModules_ShouldPreserveFixedAmount() { // assert will ensure the currently fixed value has not changed by more than 0.01%. static void testCombinations(RecipeRow row, ProductionTable table, Action assert) { foreach (EntityCrafter crafter in Database.allCrafters) { - row.entity = new(crafter, Quality.Normal); + row.entity = crafter.With(Quality.Normal); foreach (Goods fuel in crafter.energy.fuels) { - row.fuel = (fuel, Quality.Normal); + row.fuel = fuel.With(Quality.Normal); foreach (Module module in Database.allModules.Concat([null])) { ModuleTemplateBuilder builder = new(); if (module != null) { - builder.list.Add((new(module, Quality.Normal), 0)); + builder.list.Add((module.With(Quality.Normal), 0)); } row.modules = builder.Build(row); @@ -59,7 +59,7 @@ public void ChangeProductionTableModuleConfig_ShouldPreserveFixedAmount() { ProjectPage page = new ProjectPage(project, typeof(ProductionTable)); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); List modules = [.. Database.allModules.Where(m => !m.name.Contains("productivity"))]; @@ -71,10 +71,10 @@ public void ChangeProductionTableModuleConfig_ShouldPreserveFixedAmount() { // Call assert for each combination. assert will ensure the currently fixed value has not changed by more than 0.01%. void testCombinations(RecipeRow row, ProductionTable table, Action assert) { foreach (EntityCrafter crafter in Database.allCrafters) { - row.entity = new(crafter, Quality.Normal); + row.entity = crafter.With(Quality.Normal); foreach (Goods fuel in crafter.energy.fuels) { - row.fuel = (fuel, Quality.Normal); + row.fuel = fuel.With(Quality.Normal); foreach (Module module in modules) { for (int beaconCount = 0; beaconCount < 13; beaconCount++) { @@ -83,14 +83,14 @@ void testCombinations(RecipeRow row, ProductionTable table, Action assert) { // Preemptive code for if ProductionTable.modules is made writable. // The ProductionTable.modules setter must notify all relevant recipes if it is added. _ = method.Invoke(table, [new ModuleFillerParameters(table) { - beacon = new(beacon, Quality.Normal), - beaconModule = new(module, Quality.Normal), + beacon = beacon.With(Quality.Normal), + beaconModule = module.With(Quality.Normal), beaconsPerBuilding = beaconCount, }]); } else { - table.modules.beacon = new(beacon, Quality.Normal); - table.modules.beaconModule = new(module, Quality.Normal); + table.modules.beacon = beacon.With(Quality.Normal); + table.modules.beaconModule = module.With(Quality.Normal); table.modules.beaconsPerBuilding = beaconCount; } table.Solve((ProjectPage)table.owner).Wait(); @@ -117,17 +117,17 @@ public async Task AllCombinationsWithVariousFixedCounts_DisplayedProductsMatchSc int testCount = 0; // Run through all combinations of recipe, crafter, fuel, and fixed module, including all qualities. - foreach (ObjectWithQuality recipe in Database.recipes.all.WithAllQualities()) { + foreach (IObjectWithQuality recipe in Database.recipes.all.WithAllQualities()) { table.AddRecipe(recipe, DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Last(); - foreach (ObjectWithQuality crafter in Database.allCrafters.WithAllQualities()) { + foreach (IObjectWithQuality crafter in Database.allCrafters.WithAllQualities()) { row.entity = crafter; - foreach (ObjectWithQuality fuel in crafter.target.energy.fuels.WithAllQualities()) { + foreach (IObjectWithQuality fuel in crafter.target.energy.fuels.WithAllQualities()) { row.fuel = fuel; - foreach (ObjectWithQuality module in Database.allModules.WithAllQualities().Prepend(null)) { + foreach (IObjectWithQuality module in Database.allModules.WithAllQualities().Prepend(null)) { row.modules = module == null ? null : new ModuleTemplateBuilder { list = { (module, 0) } }.Build(row); do { // r.NextDouble could (at least in theory) return 0 or a value that rounds to 0. @@ -158,7 +158,7 @@ public async Task AllCombinationsWithVariousFixedCounts_DisplayedProductsMatchSc // ProductsForSolver doesn't include the spent fuel. Append an entry for the spent fuel, in the case that the spent // fuel is not a recipe product. // If the spent fuel is also a recipe product, this value will ignored in favor of the recipe-product value. - .Append(new(row.fuel.FuelResult()?.target.With(row.fuel.FuelResult().quality), 0, null, 0, null)))) { + .Append(new(row.fuel.FuelResult(), 0, null, 0, null)))) { var (solverGoods, solverAmount, _, _, _) = solver; var (displayGoods, displayAmount, _, _) = display; diff --git a/Yafc.Model.Tests/Model/ProductionTableTests.cs b/Yafc.Model.Tests/Model/ProductionTableTests.cs index a3e7f0da..a4926a91 100644 --- a/Yafc.Model.Tests/Model/ProductionTableTests.cs +++ b/Yafc.Model.Tests/Model/ProductionTableTests.cs @@ -15,7 +15,7 @@ public void ProductionTableTest_CanSaveAndLoadWithRecipe() { ProjectPage page = new(project, typeof(ProductionTable)); project.pages.Add(page); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer); ErrorCollector collector = new(); using MemoryStream stream = new(); @@ -33,7 +33,7 @@ public void ProductionTableTest_CanSaveAndLoadWithEmptySubtable() { ProjectPage page = new(project, typeof(ProductionTable)); project.pages.Add(page); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); row.subgroup = new ProductionTable(row); @@ -53,10 +53,10 @@ public void ProductionTableTest_CanSaveAndLoadWithNonemptySubtable() { ProjectPage page = new(project, typeof(ProductionTable)); project.pages.Add(page); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); row.subgroup = new ProductionTable(row); - row.subgroup.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + row.subgroup.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer); ErrorCollector collector = new(); using MemoryStream stream = new(); @@ -74,7 +74,7 @@ public void ProductionTableTest_CanLoadWithUnexpectedObject() { ProjectPage page = new(project, typeof(ProductionTable)); project.pages.Add(page); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "recipe"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "recipe").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); row.subgroup = new ProductionTable(row); diff --git a/Yafc.Model.Tests/Model/RecipeParametersTests.cs b/Yafc.Model.Tests/Model/RecipeParametersTests.cs index d3359ae7..6ba3e32b 100644 --- a/Yafc.Model.Tests/Model/RecipeParametersTests.cs +++ b/Yafc.Model.Tests/Model/RecipeParametersTests.cs @@ -14,8 +14,8 @@ public async Task FluidBoilingRecipes_HaveCorrectConsumption() { ProjectPage page = new(project, typeof(ProductionTable)); project.pages.Add(page); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe(new(Database.recipes.all.Single(r => r.name == "boiler.boiler.steam"), Quality.Normal), DataUtils.DeterministicComparer); - table.AddRecipe(new(Database.recipes.all.Single(r => r.name == "boiler.heat-exchanger.steam"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "boiler.boiler.steam").With(Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "boiler.heat-exchanger.steam").With(Quality.Normal), DataUtils.DeterministicComparer); List water = Database.fluidVariants["Fluid.water"]; diff --git a/Yafc.Model.Tests/Model/SelectableVariantsTests.cs b/Yafc.Model.Tests/Model/SelectableVariantsTests.cs index 7ad70a00..603ea504 100644 --- a/Yafc.Model.Tests/Model/SelectableVariantsTests.cs +++ b/Yafc.Model.Tests/Model/SelectableVariantsTests.cs @@ -12,7 +12,7 @@ public async Task CanSelectVariantFuel_VariantFuelChanges() { ProjectPage page = new ProjectPage(project, typeof(ProductionTable)); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe(new(Database.recipes.all.Single(r => r.name == "generator.electricity"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "generator.electricity").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); // Solve is not necessary in this test, but I'm calling it in case we decide to hide the fuel on disabled recipes. @@ -31,7 +31,7 @@ public async Task CanSelectVariantFuelWithFavorites_VariantFuelChanges() { ProjectPage page = new ProjectPage(project, typeof(ProductionTable)); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe(new(Database.recipes.all.Single(r => r.name == "generator.electricity"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "generator.electricity").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); // Solve is not necessary in this test, but I'm calling it in case we decide to hide the fuel on disabled recipes. @@ -49,7 +49,7 @@ public async Task CanSelectVariantIngredient_VariantIngredientChanges() { ProjectPage page = new ProjectPage(project, typeof(ProductionTable)); ProductionTable table = (ProductionTable)page.content; - table.AddRecipe((Database.recipes.all.Single(r => r.name == "steam_void"), Quality.Normal), DataUtils.DeterministicComparer); + table.AddRecipe(Database.recipes.all.Single(r => r.name == "steam_void").With(Quality.Normal), DataUtils.DeterministicComparer); RecipeRow row = table.GetAllRecipes().Single(); // Solve is necessary here: Disabled recipes have null ingredients (and products), and Solve is the call that updates hierarchyEnabled. diff --git a/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs b/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs index 6e1d1067..235c0de9 100644 --- a/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs +++ b/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs @@ -18,9 +18,9 @@ public class SerializationTreeChangeDetection { [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.fillerModule)] = typeof(IObjectWithQuality), + [nameof(ModuleFillerParameters.beacon)] = typeof(IObjectWithQuality), + [nameof(ModuleFillerParameters.beaconModule)] = typeof(IObjectWithQuality), [nameof(ModuleFillerParameters.beaconsPerBuilding)] = typeof(int), [nameof(ModuleFillerParameters.overrideCrafterBeacons)] = typeof(OverrideCrafterBeacons), }, @@ -35,7 +35,7 @@ public class SerializationTreeChangeDetection { [nameof(ProductionSummaryEntry.subgroup)] = typeof(ProductionSummaryGroup), }, [typeof(ProductionSummaryColumn)] = new() { - [nameof(ProductionSummaryColumn.goods)] = typeof(ObjectWithQuality), + [nameof(ProductionSummaryColumn.goods)] = typeof(IObjectWithQuality), }, [typeof(ProductionSummary)] = new() { [nameof(ProductionSummary.group)] = typeof(ProductionSummaryGroup), @@ -48,22 +48,22 @@ public class SerializationTreeChangeDetection { [nameof(ProductionTable.modules)] = typeof(ModuleFillerParameters), }, [typeof(RecipeRowCustomModule)] = new() { - [nameof(RecipeRowCustomModule.module)] = typeof(ObjectWithQuality), + [nameof(RecipeRowCustomModule.module)] = typeof(IObjectWithQuality), [nameof(RecipeRowCustomModule.fixedCount)] = typeof(int), }, [typeof(ModuleTemplate)] = new() { - [nameof(ModuleTemplate.beacon)] = typeof(ObjectWithQuality), + [nameof(ModuleTemplate.beacon)] = typeof(IObjectWithQuality), [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.recipe)] = typeof(IObjectWithQuality), + [nameof(RecipeRow.entity)] = typeof(IObjectWithQuality), + [nameof(RecipeRow.fuel)] = typeof(IObjectWithQuality), [nameof(RecipeRow.fixedBuildings)] = typeof(float), [nameof(RecipeRow.fixedFuel)] = typeof(bool), - [nameof(RecipeRow.fixedIngredient)] = typeof(ObjectWithQuality), - [nameof(RecipeRow.fixedProduct)] = typeof(ObjectWithQuality), + [nameof(RecipeRow.fixedIngredient)] = typeof(IObjectWithQuality), + [nameof(RecipeRow.fixedProduct)] = typeof(IObjectWithQuality), [nameof(RecipeRow.builtBuildings)] = typeof(int?), [nameof(RecipeRow.showTotalIO)] = typeof(bool), [nameof(RecipeRow.enabled)] = typeof(bool), @@ -73,7 +73,7 @@ public class SerializationTreeChangeDetection { [nameof(RecipeRow.variants)] = typeof(HashSet), }, [typeof(ProductionLink)] = new() { - [nameof(ProductionLink.goods)] = typeof(ObjectWithQuality), + [nameof(ProductionLink.goods)] = typeof(IObjectWithQuality), [nameof(ProductionLink.amount)] = typeof(float), [nameof(ProductionLink.algorithm)] = typeof(LinkAlgorithm), }, @@ -131,30 +131,10 @@ public class SerializationTreeChangeDetection { [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.beacon)] = typeof(IObjectWithQuality), [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), + [nameof(BeaconOverrideConfiguration.beaconModule)] = typeof(IObjectWithQuality), }, }; diff --git a/Yafc.Model.Tests/TestHelpers.cs b/Yafc.Model.Tests/TestHelpers.cs index 98e8626e..088ad889 100644 --- a/Yafc.Model.Tests/TestHelpers.cs +++ b/Yafc.Model.Tests/TestHelpers.cs @@ -10,6 +10,6 @@ internal static class TestHelpers { /// The type of the input objects. Must be or one of its subclasses. /// The sequence of values that should have qualities applied to them. /// The cartesian product of and the members of . - public static IEnumerable> WithAllQualities(this IEnumerable values) where T : FactorioObject + public static IEnumerable> WithAllQualities(this IEnumerable values) where T : FactorioObject => values.SelectMany(c => Database.qualities.all.Select(q => c.With(q))).Distinct(); } diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index d7ececcd..c6e616d3 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; -using System.Text.Json; using Yafc.UI; [assembly: InternalsVisibleTo("Yafc.Parser")] @@ -661,7 +661,7 @@ public static bool CanAcceptModule(ModuleSpecification module, AllowedEffects ef return true; } - public bool CanAcceptModule(ObjectWithQuality module) where T : Module => CanAcceptModule(module.target.moduleSpecification); + public bool CanAcceptModule(IObjectWithQuality module) where T : Module => CanAcceptModule(module.target.moduleSpecification); public bool CanAcceptModule(ModuleSpecification module) => CanAcceptModule(module, allowedEffects, allowedModuleCategories); } @@ -796,7 +796,9 @@ public override DependencyNode GetDependencies() { /// /// Represents a with an attached modifier. /// -/// The concrete type of the quality-modified object. +/// The type of the quality-modified object. +/// Like s and their derived types, any two values may be compared using +/// == and the default reference equality. public interface IObjectWithQuality : IFactorioObjectWrapper where T : FactorioObject { /// /// Gets the object managed by this instance. @@ -809,70 +811,78 @@ public interface IObjectWithQuality : IFactorioObjectWrapper where T : Fa } /// -/// Represents a with an attached modifier. +/// Provides static methods for interacting with the canonical quality objects. /// -/// The concrete type of the quality-modified object. -/// The object to be associated with a quality modifier. If this parameter might be , -/// use the implicit conversion from instead. -/// The quality for this object. -[Serializable] -public sealed class ObjectWithQuality(T target, Quality quality) : IObjectWithQuality, ICustomJsonDeserializer> where T : FactorioObject { - // These items do not support quality: +/// References to the old ObjectWithQuality<T> class should be changed to , and constructor +/// calls and conversions should be replaced with calls to . +public static class ObjectWithQuality { + private static readonly Dictionary<(FactorioObject, Quality), object> _cache = []; + // These items do not support quality private static readonly HashSet nonQualityItemNames = ["science", "item-total-input", "item-total-output"]; - /// - public T target { get; } = target ?? throw new ArgumentNullException(nameof(target)); - /// - public Quality quality { get; } = CheckQuality(target, quality ?? throw new ArgumentNullException(nameof(quality))); - - private static Quality CheckQuality(T target, Quality quality) => target switch { - // Things that don't support quality: - Fluid or Location or Mechanics { source: Entity } or Quality or Special or Technology or Tile => Quality.Normal, - Recipe r when r.ingredients.All(i => i.goods is Fluid) => Quality.Normal, - // Most other things support quality, but a few items are excluded: - Item when nonQualityItemNames.Contains(target.name) => Quality.Normal, - _ => quality - }; - /// - /// Creates a new with the current and the specified . + /// Gets the representing a with an attached modifier. /// - public ObjectWithQuality With(Quality quality) => new(target, quality); - - string IFactorioObjectWrapper.text => ((IFactorioObjectWrapper)target).text; - FactorioObject IFactorioObjectWrapper.target => target; - float IFactorioObjectWrapper.amount => ((IFactorioObjectWrapper)target).amount; - - public static implicit operator ObjectWithQuality?((T? entity, Quality quality) value) => value.entity == null ? null : new(value.entity, value.quality); + /// The type of the quality-modified object. + /// The object to be associated with a quality modifier. + /// The quality for this object. + /// For shorter expressions/lines, consider calling instead. + [return: NotNullIfNotNull(nameof(target))] + public static IObjectWithQuality? Get(T? target, Quality quality) where T : FactorioObject + => target == null ? null : (IObjectWithQuality)_cache[(target, quality ?? throw new ArgumentNullException(nameof(quality)))]; - /// - public static bool Deserialize(ref Utf8JsonReader reader, DeserializationContext context, out ObjectWithQuality? result) { - if (reader.TokenType == JsonTokenType.String) { - // Read the old `"entity": "Entity.solar-panel"` format. - if (Database.objectsByTypeName[reader.GetString()!] is not T obj) { - context.Error($"Could not convert '{reader.GetString()}' to a {typeof(T).Name}.", ErrorSeverity.MinorDataLoss); - result = null; - return true; + /// + /// Constructs all s for the current contents of data.raw. This should only be called by + /// FactorioDataDeserializer.LoadData. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static void LoadCache(List allObjects) { + _cache.Clear(); + // Quality.Normal must be processed first, so the reset-to-normal logic will work properly. + foreach (var quality in allObjects.OfType().Where(x => x != Quality.Normal).Prepend(Quality.Normal)) { + foreach (var obj in allObjects) { + MakeObject(obj, quality); } - result = new(obj, Quality.Normal); - return true; } - // This is probably the current `"entity": { "target": "Entity.solar-panel", "quality": "Quality.rare" }` format. - result = default; - return false; } - // Ensure ObjectWithQuality equals ObjectWithQuality as long as the properties are equal, regardless of X and Y. - public override bool Equals(object? obj) => Equals(obj as IObjectWithQuality); - public bool Equals(IObjectWithQuality? other) => other is not null && target == other.target && quality == other.quality; - public override int GetHashCode() => HashCode.Combine(target, quality); - - public static bool operator ==(ObjectWithQuality? left, ObjectWithQuality? right) => left == (IObjectWithQuality?)right; - public static bool operator ==(ObjectWithQuality? left, IObjectWithQuality? right) => (left is null && right is null) || (left is not null && left.Equals(right)); - public static bool operator ==(IObjectWithQuality? left, ObjectWithQuality? right) => right == left; - public static bool operator !=(ObjectWithQuality? left, ObjectWithQuality? right) => !(left == right); - public static bool operator !=(ObjectWithQuality? left, IObjectWithQuality? right) => !(left == right); - public static bool operator !=(IObjectWithQuality? left, ObjectWithQuality? right) => !(left == right); + private static void MakeObject(FactorioObject target, Quality quality) { + Quality realQuality = target switch { + // Things that don't support quality: + Fluid or Location or Mechanics { source: Entity } or Quality or Special or Technology or Tile => Quality.Normal, + Recipe r when r.ingredients.All(i => i.goods is Fluid) => Quality.Normal, + Item when nonQualityItemNames.Contains(target.name) => Quality.Normal, + // Everything else supports quality + _ => quality + }; + + object withQuality; + if (realQuality == quality) { + Type type = typeof(ConcreteObjectWithQuality<>).MakeGenericType(target.GetType()); + withQuality = Activator.CreateInstance(type, [target, realQuality])!; + } + else { + // This got changed back to normal quality. Get the previously stored object instead of making a new one. + withQuality = Get(target, realQuality); + } + + _cache[(target, quality)] = withQuality; + } + + /// + /// Represents a with an attached modifier. + /// + /// The concrete type of the quality-modified object. + private sealed class ConcreteObjectWithQuality(T target, Quality quality) : IObjectWithQuality where T : FactorioObject { + /// + public T target { get; } = target ?? throw new ArgumentNullException(nameof(target)); + /// + public Quality quality { get; } = quality ?? throw new ArgumentNullException(nameof(quality)); + + string IFactorioObjectWrapper.text => ((IFactorioObjectWrapper)target).text; + FactorioObject IFactorioObjectWrapper.target => target; + float IFactorioObjectWrapper.amount => ((IFactorioObjectWrapper)target).amount; + } } public class Effect { diff --git a/Yafc.Model/Data/Database.cs b/Yafc.Model/Data/Database.cs index f4d56112..f2088faa 100644 --- a/Yafc.Model/Data/Database.cs +++ b/Yafc.Model/Data/Database.cs @@ -12,13 +12,13 @@ public static class Database { public static Item[] allSciencePacks { get; internal set; } = null!; public static Dictionary objectsByTypeName { get; internal set; } = null!; public static Dictionary> fluidVariants { get; internal set; } = null!; - public static ObjectWithQuality voidEnergy { get; internal set; } = null!; - public static ObjectWithQuality science { get; internal set; } = null!; - public static ObjectWithQuality itemInput { get; internal set; } = null!; - public static ObjectWithQuality itemOutput { get; internal set; } = null!; - public static ObjectWithQuality electricity { get; internal set; } = null!; - public static ObjectWithQuality electricityGeneration { get; internal set; } = null!; - public static ObjectWithQuality heat { get; internal set; } = null!; + public static IObjectWithQuality voidEnergy { get; internal set; } = null!; + public static IObjectWithQuality science { get; internal set; } = null!; + public static IObjectWithQuality itemInput { get; internal set; } = null!; + public static IObjectWithQuality itemOutput { get; internal set; } = null!; + public static IObjectWithQuality electricity { get; internal set; } = null!; + public static IObjectWithQuality electricityGeneration { get; internal set; } = null!; + public static IObjectWithQuality heat { get; internal set; } = null!; public static Entity? character { get; internal set; } public static EntityCrafter[] allCrafters { get; internal set; } = null!; public static Module[] allModules { get; internal set; } = null!; diff --git a/Yafc.Model/Model/ModuleFillerParameters.cs b/Yafc.Model/Model/ModuleFillerParameters.cs index f07d23a5..94262fde 100644 --- a/Yafc.Model/Model/ModuleFillerParameters.cs +++ b/Yafc.Model/Model/ModuleFillerParameters.cs @@ -12,7 +12,7 @@ namespace Yafc.Model; /// The number of beacons to use. The total number of modules in beacons is this value times the number of modules that can be placed in a beacon. /// The module to place in the beacon. [Serializable] -public record BeaconOverrideConfiguration(ObjectWithQuality beacon, int beaconCount, ObjectWithQuality beaconModule); +public record BeaconOverrideConfiguration(IObjectWithQuality beacon, int beaconCount, IObjectWithQuality beaconModule); /// /// The result of applying the various beacon preferences to a crafter; this may result in a desired configuration where the beacon or module is not specified. @@ -21,7 +21,7 @@ public record BeaconOverrideConfiguration(ObjectWithQuality beacon /// The number of beacons to use. The total number of modules in beacons is this value times the number of modules that can be placed in a beacon. /// The module to place in the beacon, or if no beacons or beacon modules should be used. [Serializable] -public record BeaconConfiguration(ObjectWithQuality? beacon, int beaconCount, ObjectWithQuality? beaconModule) { +public record BeaconConfiguration(IObjectWithQuality? beacon, int beaconCount, IObjectWithQuality? beaconModule) { public static implicit operator BeaconConfiguration(BeaconOverrideConfiguration beaconConfiguration) => new(beaconConfiguration.beacon, beaconConfiguration.beaconCount, beaconConfiguration.beaconModule); } @@ -33,9 +33,9 @@ public static implicit operator BeaconConfiguration(BeaconOverrideConfiguration public class ModuleFillerParameters : ModelObject { private bool _fillMiners; private float _autoFillPayback; - private ObjectWithQuality? _fillerModule; - private ObjectWithQuality? _beacon; - private ObjectWithQuality? _beaconModule; + private IObjectWithQuality? _fillerModule; + private IObjectWithQuality? _beacon; + private IObjectWithQuality? _beaconModule; private int _beaconsPerBuilding = 8; public ModuleFillerParameters(ModelObject owner) : base(owner) => overrideCrafterBeacons.OverrideSettingChanging += ModuleFillerParametersChanging; @@ -48,15 +48,15 @@ public float autoFillPayback { get => _autoFillPayback; set => ChangeModuleFillerParameters(ref _autoFillPayback, value); } - public ObjectWithQuality? fillerModule { + public IObjectWithQuality? fillerModule { get => _fillerModule; set => ChangeModuleFillerParameters(ref _fillerModule, value); } - public ObjectWithQuality? beacon { + public IObjectWithQuality? beacon { get => _beacon; set => ChangeModuleFillerParameters(ref _beacon, value); } - public ObjectWithQuality? beaconModule { + public IObjectWithQuality? beaconModule { get => _beaconModule; set => ChangeModuleFillerParameters(ref _beaconModule, value); } @@ -114,10 +114,10 @@ public BeaconConfiguration GetBeaconsForCrafter(EntityCrafter? crafter) { internal void AutoFillBeacons(RecipeOrTechnology recipe, EntityCrafter entity, ref ModuleEffects effects, ref UsedModule used) { BeaconConfiguration beaconsToUse = GetBeaconsForCrafter(entity); - if (!recipe.flags.HasFlags(RecipeFlags.UsesMiningProductivity) && beaconsToUse.beacon is ObjectWithQuality beaconQual && beaconsToUse.beaconModule != null) { - EntityBeacon beacon = beaconQual.target; - effects.AddModules(beaconsToUse.beaconModule, beaconsToUse.beaconCount * beaconQual.GetBeaconEfficiency() * beacon.GetProfile(beaconsToUse.beaconCount) * beacon.moduleSlots, entity.allowedEffects); - used.beacon = beaconQual; + if (!recipe.flags.HasFlags(RecipeFlags.UsesMiningProductivity) && beaconsToUse.beacon != null && beaconsToUse.beaconModule != null) { + EntityBeacon beacon = beaconsToUse.beacon.target; + effects.AddModules(beaconsToUse.beaconModule, beaconsToUse.beaconCount * beaconsToUse.beacon.GetBeaconEfficiency() * beacon.GetProfile(beaconsToUse.beaconCount) * beacon.moduleSlots, entity.allowedEffects); + used.beacon = beaconsToUse.beacon; used.beaconCount = beaconsToUse.beaconCount; } } @@ -127,7 +127,7 @@ private void AutoFillModules((float recipeTime, float fuelUsagePerSecondPerBuild Quality quality = Quality.MaxAccessible; - ObjectWithQuality recipe = row.recipe; + IObjectWithQuality recipe = row.recipe; if (autoFillPayback > 0 && (fillMiners || !recipe.target.flags.HasFlags(RecipeFlags.UsesMiningProductivity))) { /* @@ -161,7 +161,7 @@ The payback time is calculated as the module cost divided by the economy gain pe } float bestEconomy = 0f; - ObjectWithQuality? usedModule = null; + IObjectWithQuality? usedModule = null; foreach (var module in Database.allModules) { if (module.IsAccessibleWithCurrentMilestones() && entity.CanAcceptModule(module.moduleSpecification) && recipe.target.CanAcceptModule(module)) { @@ -171,7 +171,7 @@ The payback time is calculated as the module cost divided by the economy gain pe if (economy > bestEconomy && module.Cost() / economy <= autoFillPayback) { bestEconomy = economy; - usedModule = new(module, quality); + usedModule = module.With(quality); } } } @@ -198,7 +198,7 @@ internal void GetModulesInfo((float recipeTime, float fuelUsagePerSecondPerBuild AutoFillModules(partialParams, row, entity, ref effects, ref used); } - private static void AddModuleSimple(ObjectWithQuality module, ref ModuleEffects effects, EntityCrafter entity, ref UsedModule used) { + private static void AddModuleSimple(IObjectWithQuality module, ref ModuleEffects effects, EntityCrafter entity, ref UsedModule used) { int fillerLimit = effects.GetModuleSoftLimit(module, entity.moduleSlots); effects.AddModules(module, fillerLimit); used.modules = [(module, fillerLimit, false)]; diff --git a/Yafc.Model/Model/ProductionSummary.cs b/Yafc.Model/Model/ProductionSummary.cs index f41ec6c3..9f7272da 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, ObjectWithQuality goods) : ModelObject(owner) { - public ObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Object does not exist"); +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 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, new(Database.electricity.target, Quality.Normal))); + columns.Add(new ProductionSummaryColumn(this, Database.electricity)); base.InitNew(); } diff --git a/Yafc.Model/Model/ProductionTable.GenuineRecipe.cs b/Yafc.Model/Model/ProductionTable.GenuineRecipe.cs index a2c2995c..e921a61b 100644 --- a/Yafc.Model/Model/ProductionTable.GenuineRecipe.cs +++ b/Yafc.Model/Model/ProductionTable.GenuineRecipe.cs @@ -35,8 +35,8 @@ public bool FindLink(IObjectWithQuality goods, [MaybeNullWhen(false)] out } // Pass all remaining calls through to the underlying RecipeRow. - public ObjectWithQuality? entity => row.entity; - public ObjectWithQuality? fuel => row.fuel; + public IObjectWithQuality? entity => row.entity; + public IObjectWithQuality? fuel => row.fuel; public float fixedBuildings => row.fixedBuildings; public double recipesPerSecond { get => row.recipesPerSecond; set => row.recipesPerSecond = value; } public float RecipeTime => ((IRecipeRow)row).RecipeTime; diff --git a/Yafc.Model/Model/ProductionTable.ImplicitLink.cs b/Yafc.Model/Model/ProductionTable.ImplicitLink.cs index c409586e..5b6e3ff7 100644 --- a/Yafc.Model/Model/ProductionTable.ImplicitLink.cs +++ b/Yafc.Model/Model/ProductionTable.ImplicitLink.cs @@ -6,17 +6,17 @@ public partial class ProductionTable { /// /// An implicitly-created link, used to ensure synthetic recipes are properly connected to the rest of the production table. /// - /// The linked . + /// The linked . /// The that owns this link. /// The ordinary link that caused the creation of this implicit link, and the link that will be displayed if the /// user requests the summary for this link. - private class ImplicitLink(ObjectWithQuality goods, ProductionTable owner, ProductionLink displayLink) : IProductionLink { + private class ImplicitLink(IObjectWithQuality goods, ProductionTable owner, ProductionLink displayLink) : IProductionLink { /// /// Always ; implicit links never allow over/under production. /// public LinkAlgorithm algorithm => LinkAlgorithm.Match; - public ObjectWithQuality goods { get; } = goods; + public IObjectWithQuality goods { get; } = goods; /// /// Always 0; implicit links never request additional production or consumption. diff --git a/Yafc.Model/Model/ProductionTable.ScienceDecomposition.cs b/Yafc.Model/Model/ProductionTable.ScienceDecomposition.cs index 5e8abb29..8425c6f3 100644 --- a/Yafc.Model/Model/ProductionTable.ScienceDecomposition.cs +++ b/Yafc.Model/Model/ProductionTable.ScienceDecomposition.cs @@ -13,9 +13,9 @@ public partial class ProductionTable { /// The (genuine) link for the output pack. /// The (implicit) link for the input pack. private class ScienceDecomposition(Goods pack, Quality quality, IProductionLink productLink, ImplicitLink ingredientLink) : IRecipeRow { - public ObjectWithQuality? entity => null; + public IObjectWithQuality? entity => null; - public ObjectWithQuality? fuel => null; + public IObjectWithQuality? fuel => null; /// /// Always 0; the solver must scale this recipe to match the available quality packs. diff --git a/Yafc.Model/Model/ProductionTable.cs b/Yafc.Model/Model/ProductionTable.cs index fec78aea..6d0ab6c2 100644 --- a/Yafc.Model/Model/ProductionTable.cs +++ b/Yafc.Model/Model/ProductionTable.cs @@ -161,7 +161,7 @@ public void RebuildLinkMap() { // If the link is at the current level, if (link.owner == this) { foreach (var quality in Database.qualities.all) { - ObjectWithQuality pack = goods.With(quality); + IObjectWithQuality pack = goods.With(quality); // and there is unlinked production here or anywhere deeper: (This test only succeeds for non-normal qualities) if (unlinkedProduction.Remove(pack)) { // Remove the quality pack, since it's about to be linked, // and create the decomposition. @@ -251,15 +251,15 @@ public bool Search(SearchQuery query) { /// If not , this method will select a crafter or lab that can use this fuel, assuming such an entity exists. /// For example, if the selected fuel is coal, the recipe will be configured with a burner assembler/lab if any are available. /// - public void AddRecipe(ObjectWithQuality recipe, IComparer ingredientVariantComparer, - ObjectWithQuality? selectedFuel = null, IObjectWithQuality? spentFuel = null) { + public void AddRecipe(IObjectWithQuality recipe, IComparer ingredientVariantComparer, + IObjectWithQuality? selectedFuel = null, IObjectWithQuality? spentFuel = null) { RecipeRow recipeRow = new RecipeRow(this, recipe); this.RecordUndo().recipes.Add(recipeRow); EntityCrafter? selectedFuelCrafter = GetSelectedFuelCrafter(recipe.target, selectedFuel); EntityCrafter? spentFuelRecipeCrafter = GetSpentFuelCrafter(recipe.target, spentFuel); - recipeRow.entity = (selectedFuelCrafter ?? spentFuelRecipeCrafter ?? recipe.target.crafters.AutoSelect(DataUtils.FavoriteCrafter), Quality.Normal); + recipeRow.entity = (selectedFuelCrafter ?? spentFuelRecipeCrafter ?? recipe.target.crafters.AutoSelect(DataUtils.FavoriteCrafter)).With(Quality.Normal); if (recipeRow.entity != null) { recipeRow.fuel = GetSelectedFuel(selectedFuel, recipeRow) @@ -275,7 +275,7 @@ public void AddRecipe(ObjectWithQuality recipe, IComparer? selectedFuel) => + private static EntityCrafter? GetSelectedFuelCrafter(RecipeOrTechnology recipe, IObjectWithQuality? selectedFuel) => selectedFuel?.target.fuelFor.OfType() .Where(e => e.recipes.Contains(recipe)) .AutoSelect(DataUtils.FavoriteCrafter); @@ -290,7 +290,7 @@ public void AddRecipe(ObjectWithQuality recipe, IComparer? GetFuelForSpentFuel(IObjectWithQuality? spentFuel, [NotNull] RecipeRow recipeRow) { + private static IObjectWithQuality? GetFuelForSpentFuel(IObjectWithQuality? spentFuel, [NotNull] RecipeRow recipeRow) { if (spentFuel is null) { return null; } @@ -299,7 +299,7 @@ public void AddRecipe(ObjectWithQuality recipe, IComparer? GetSelectedFuel(ObjectWithQuality? selectedFuel, [NotNull] RecipeRow recipeRow) => + private static IObjectWithQuality? GetSelectedFuel(IObjectWithQuality? selectedFuel, [NotNull] RecipeRow recipeRow) => // Skipping AutoSelect since there will only be one result at most. recipeRow.entity?.target.energy?.fuels.FirstOrDefault(e => e == selectedFuel?.target)?.With(selectedFuel!.quality); diff --git a/Yafc.Model/Model/ProductionTableContent.cs b/Yafc.Model/Model/ProductionTableContent.cs index 19a8e0ec..0ab21834 100644 --- a/Yafc.Model/Model/ProductionTableContent.cs +++ b/Yafc.Model/Model/ProductionTableContent.cs @@ -16,7 +16,7 @@ public struct ModuleEffects { public readonly float speedMod => MathF.Max(1f + speed, 0.2f); public readonly float energyUsageMod => MathF.Max(1f + consumption, 0.2f); public readonly float qualityMod => MathF.Max(quality, 0); - public void AddModules(ObjectWithQuality module, float count, AllowedEffects allowedEffects = AllowedEffects.All) { + public void AddModules(IObjectWithQuality module, float count, AllowedEffects allowedEffects = AllowedEffects.All) { ModuleSpecification spec = module.target.moduleSpecification; Quality quality = module.quality; if (allowedEffects.HasFlags(AllowedEffects.Speed)) { @@ -36,7 +36,7 @@ public void AddModules(ObjectWithQuality module, float count, AllowedEff } } - public readonly int GetModuleSoftLimit(ObjectWithQuality module, int hardLimit) { + public readonly int GetModuleSoftLimit(IObjectWithQuality module, int hardLimit) { ModuleSpecification spec = module.target.moduleSpecification; Quality quality = module.quality; @@ -56,8 +56,8 @@ public readonly int GetModuleSoftLimit(ObjectWithQuality module, int har /// One module that is (or will be) applied to a , and the number of times it should appear. /// /// Immutable. To modify, modify the owning . -public class RecipeRowCustomModule(ModuleTemplate owner, ObjectWithQuality module, int fixedCount = 0) : ModelObject(owner) { - public ObjectWithQuality module { get; } = module ?? throw new ArgumentNullException(nameof(module)); +public class RecipeRowCustomModule(ModuleTemplate owner, IObjectWithQuality module, int fixedCount = 0) : ModelObject(owner) { + public IObjectWithQuality module { get; } = module ?? throw new ArgumentNullException(nameof(module)); public int fixedCount { get; } = fixedCount; } @@ -70,7 +70,7 @@ public class ModuleTemplate : ModelObject { /// /// The beacon to use, if any, for the associated . /// - public ObjectWithQuality? beacon { get; } + public IObjectWithQuality? beacon { get; } /// /// The modules, if any, to directly insert into the crafting entity. /// @@ -80,7 +80,7 @@ public class ModuleTemplate : ModelObject { /// public ReadOnlyCollection beaconList { get; private set; } = new([]); // Must be a distinct collection object to accommodate the deserializer. - private ModuleTemplate(ModelObject owner, ObjectWithQuality? beacon) : base(owner) => this.beacon = beacon; + private ModuleTemplate(ModelObject owner, IObjectWithQuality? beacon) : base(owner) => this.beacon = beacon; public bool IsCompatibleWith([NotNullWhen(true)] RecipeRow? row) { if (row?.entity == null) { @@ -111,9 +111,9 @@ public bool IsCompatibleWith([NotNullWhen(true)] RecipeRow? row) { } internal void GetModulesInfo(RecipeRow row, EntityCrafter entity, ref ModuleEffects effects, ref UsedModule used, ModuleFillerParameters? filler) { - List<(ObjectWithQuality module, int count, bool beacon)> buffer = []; + List<(IObjectWithQuality module, int count, bool beacon)> buffer = []; int beaconedModules = 0; - ObjectWithQuality? nonBeacon = null; + IObjectWithQuality? nonBeacon = null; used.modules = null; int remaining = entity.moduleSlots; @@ -186,7 +186,7 @@ internal static ModuleTemplate Build(ModelObject owner, ModuleTemplateBuilder bu return modules; - ReadOnlyCollection convertList(List<(ObjectWithQuality module, int fixedCount)> list) + ReadOnlyCollection convertList(List<(IObjectWithQuality module, int fixedCount)> list) => list.Select(m => new RecipeRowCustomModule(modules, m.module, m.fixedCount)).ToList().AsReadOnly(); } } @@ -198,15 +198,15 @@ public class ModuleTemplateBuilder { /// /// The beacon to be stored in after building. /// - public ObjectWithQuality? beacon { get; set; } + public IObjectWithQuality? beacon { get; set; } /// /// The list of s and counts to be stored in after building. /// - public List<(ObjectWithQuality module, int fixedCount)> list { get; set; } = []; + public List<(IObjectWithQuality module, int fixedCount)> list { get; set; } = []; /// /// The list of s and counts to be stored in after building. /// - public List<(ObjectWithQuality module, int fixedCount)> beaconList { get; set; } = []; + public List<(IObjectWithQuality module, int fixedCount)> beaconList { get; set; } = []; /// /// Builds a from this . @@ -257,8 +257,8 @@ public interface IGroupedElement { /// public interface IRecipeRow { // Variable (user-configured, for RecipeRow) properties - ObjectWithQuality? entity { get; } - ObjectWithQuality? fuel { get; } + IObjectWithQuality? entity { get; } + IObjectWithQuality? fuel { get; } /// /// If not zero, the fixed building count to be used by the solver. /// @@ -304,15 +304,15 @@ public interface IRecipeRow { /// Represents a row in a production table that can be configured by the user. /// public class RecipeRow : ModelObject, IGroupedElement, IRecipeRow { - private ObjectWithQuality? _entity; - private ObjectWithQuality? _fuel; + private IObjectWithQuality? _entity; + private IObjectWithQuality? _fuel; private float _fixedBuildings; - private ObjectWithQuality? _fixedProduct; + private IObjectWithQuality? _fixedProduct; private ModuleTemplate? _modules; - public ObjectWithQuality recipe { get; } + public IObjectWithQuality recipe { get; } // Variable parameters - public ObjectWithQuality? entity { + public IObjectWithQuality? entity { get => _entity; set { if (_entity == value) { @@ -344,7 +344,7 @@ public ObjectWithQuality? entity { } } } - public ObjectWithQuality? fuel { + public IObjectWithQuality? fuel { get => _fuel; set { if (SerializationMap.IsDeserializing || fixedBuildings == 0 || _fuel == value) { @@ -404,11 +404,11 @@ public float fixedBuildings { /// /// If not , is set to control the consumption of this ingredient. /// - public ObjectWithQuality? fixedIngredient { get; set; } + public IObjectWithQuality? fixedIngredient { get; set; } /// /// If not , is set to control the production of this product. /// - public ObjectWithQuality? fixedProduct { + public IObjectWithQuality? fixedProduct { get => _fixedProduct; set { if (value == null) { @@ -416,7 +416,7 @@ public ObjectWithQuality? fixedProduct { } else { // This takes advantage of the fact that ObjectWithQuality automatically downgrades high-quality fluids (etc.) to normal. - var products = recipe.target.products.AsEnumerable().Select(p => new ObjectWithQuality(p.goods, recipe.quality)); + var products = recipe.target.products.AsEnumerable().Select(p => p.goods.With(recipe.quality)); if (value != Database.itemOutput && products.All(p => p != value)) { // The UI doesn't know the difference between a product and a spent fuel, but we care about the difference _fixedProduct = null; @@ -457,7 +457,7 @@ public ObjectWithQuality? fixedProduct { public Module module { set { if (value != null) { - modules = new ModuleTemplateBuilder { list = { (new(value, Quality.Normal), 0) } }.Build(this); + modules = new ModuleTemplateBuilder { list = { (value.With(Quality.Normal), 0) } }.Build(this); } } } @@ -498,7 +498,7 @@ private IEnumerable BuildIngredients(bool forSolver) { float factor = forSolver ? 1 : (float)recipesPerSecond; // The solver needs the ingredients for one recipe, to produce recipesPerSecond. for (int i = 0; i < recipe.target.ingredients.Length; i++) { Ingredient ingredient = recipe.target.ingredients[i]; - ObjectWithQuality option = (ingredient.variants == null ? ingredient.goods : GetVariant(ingredient.variants)).With(recipe.quality); + IObjectWithQuality option = (ingredient.variants == null ? ingredient.goods : GetVariant(ingredient.variants)).With(recipe.quality); yield return (option, ingredient.amount * factor, links.ingredients[i], i); } } @@ -566,8 +566,9 @@ private IEnumerable BuildProducts(bool forSolver) { } if (!handledFuel) { - // null-forgiving: handledFuel is always true if spentFuel is null. - yield return (new(spentFuel!.target, spentFuel.quality), parameters.fuelUsagePerSecondPerRecipe * factor, links.spentFuel, 0, null); + // null-forgiving (both): handledFuel is always false when running the solver. + // equivalently: We do not enter this block when a non-null Goods is required. + yield return (spentFuel!, parameters.fuelUsagePerSecondPerRecipe * factor, links.spentFuel, 0, null); } } @@ -628,7 +629,7 @@ public void ChangeVariant(T was, T now) where T : FactorioObject { public float buildingCount => (float)recipesPerSecond * parameters.recipeTime; public bool visible { get; internal set; } = true; - public RecipeRow(ProductionTable owner, ObjectWithQuality recipe) : base(owner) { + public RecipeRow(ProductionTable owner, IObjectWithQuality recipe) : base(owner) { this.recipe = recipe ?? throw new ArgumentNullException(nameof(recipe), "Recipe does not exist"); links = new RecipeLinks { @@ -648,7 +649,7 @@ public void RemoveFixedModules() { CreateUndoSnapshot(); modules = null; } - public void SetFixedModule(ObjectWithQuality module) { + public void SetFixedModule(IObjectWithQuality module) { ModuleTemplateBuilder builder = modules?.GetBuilder() ?? new(); builder.list = [(module, 0)]; this.RecordUndo().modules = builder.Build(this); @@ -699,7 +700,7 @@ internal void GetModulesInfo((float recipeTime, float fuelUsagePerSecondPerBuild /// private class ChangeModulesOrEntity : IDisposable { private readonly RecipeRow row; - private readonly ObjectWithQuality? oldFuel; + private readonly IObjectWithQuality? oldFuel; private readonly RecipeParameters oldParameters; public ChangeModulesOrEntity(RecipeRow row) { @@ -793,7 +794,7 @@ public enum LinkAlgorithm { /// public interface IProductionLink { internal LinkAlgorithm algorithm { get; } - ObjectWithQuality goods { get; } + IObjectWithQuality goods { get; } float amount { get; } internal int solverIndex { get; set; } ProductionLink.Flags flags { get; set; } @@ -815,7 +816,7 @@ public interface IProductionLink { /// /// A Link is goods whose production and consumption is attempted to be balanced by YAFC across the sheet. /// -public class ProductionLink(ProductionTable group, ObjectWithQuality goods) : ModelObject(group), IProductionLink { +public class ProductionLink(ProductionTable group, IObjectWithQuality goods) : ModelObject(group), IProductionLink { [Flags] public enum Flags { // This enum uses powers of two to represent its state. @@ -840,7 +841,7 @@ public enum Flags { HasProductionAndConsumption = HasProduction | HasConsumption, } - public ObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Linked product does not exist"); + public IObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Linked product does not exist"); public float amount { get; set; } public LinkAlgorithm algorithm { get; set; } public UnitOfMeasure flowUnitOfMeasure => goods.target.flowUnitOfMeasure; @@ -897,7 +898,7 @@ public IEnumerable LinkWarnings { /// /// An ingredient for a recipe row, as reported to the UI. /// -public record RecipeRowIngredient(ObjectWithQuality? Goods, float Amount, ProductionLink? Link, Goods[]? Variants) { +public record RecipeRowIngredient(IObjectWithQuality? Goods, float Amount, ProductionLink? Link, Goods[]? Variants) { /// /// Convert from a (the form initially generated when reporting ingredients) to a /// . @@ -909,7 +910,7 @@ internal static RecipeRowIngredient FromSolver(SolverIngredient value) /// /// A product from a recipe row, as reported to the UI. /// -public record RecipeRowProduct(ObjectWithQuality? Goods, float Amount, IProductionLink? Link, float? PercentSpoiled) { +public record RecipeRowProduct(IObjectWithQuality? Goods, float Amount, IProductionLink? Link, float? PercentSpoiled) { /// /// Convert from a (the form initially generated when reporting products) to a . /// @@ -921,8 +922,8 @@ internal static RecipeRowProduct FromSolver(SolverProduct value) /// An ingredient for a recipe row, as reported to the solver. /// Alternatively, an intermediate value that will be used by the UI after conversion using . /// -internal record SolverIngredient(ObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex) { - public static implicit operator SolverIngredient((ObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex) value) +internal record SolverIngredient(IObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex) { + public static implicit operator SolverIngredient((IObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex) value) => new(value.Goods, value.Amount, value.Link, value.LinkIndex); } @@ -930,7 +931,7 @@ public static implicit operator SolverIngredient((ObjectWithQuality Goods /// A product for a recipe row, as reported to the solver. /// Alternatively, an intermediate value that will be used by the UI after conversion using . /// -internal record SolverProduct(ObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex, float? PercentSpoiled) { - public static implicit operator SolverProduct((ObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex, float? PercentSpoiled) value) +internal record SolverProduct(IObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex, float? PercentSpoiled) { + public static implicit operator SolverProduct((IObjectWithQuality Goods, float Amount, IProductionLink? Link, int LinkIndex, float? PercentSpoiled) value) => new(value.Goods, value.Amount, value.Link, value.LinkIndex, value.PercentSpoiled); } diff --git a/Yafc.Model/Model/QualityExtensions.cs b/Yafc.Model/Model/QualityExtensions.cs index fd4675a9..f4d29b2c 100644 --- a/Yafc.Model/Model/QualityExtensions.cs +++ b/Yafc.Model/Model/QualityExtensions.cs @@ -28,7 +28,7 @@ public static float StormPotentialPerTick(this IObjectWithQuality obj) => obj.target.name + "@" + obj.quality.name; public static IObjectWithQuality? mainProduct(this IObjectWithQuality recipe) => - (ObjectWithQuality?)(recipe.target.mainProduct, recipe.quality); + recipe.target.mainProduct.With(recipe.quality); public static bool CanAcceptModule(this IObjectWithQuality recipe, Module module) => recipe.target.CanAcceptModule(module); @@ -38,43 +38,44 @@ public static bool CanAcceptModule(this IObjectWithQuality r public static IObjectWithQuality? FuelResult(this IObjectWithQuality? goods) => (goods?.target as Item)?.fuelResult.With(goods!.quality); // null-forgiving: With is not called if goods is null. - [return: NotNullIfNotNull(nameof(obj))] - public static ObjectWithQuality? With(this T? obj, Quality quality) where T : FactorioObject => (obj, quality); + /// + /// Gets the representing a with an attached modifier. + /// + /// The type of the quality-oblivious object. This must be or a derived type. + /// The quality-oblivious object to be converted, or to return . + /// The desired quality. If does not accept quality modifiers (e.g. fluids or technologies), + /// the return value will have normal quality regardless of the value of this parameter. This parameter must still be a valid + /// (not ), even if it will be ignored. + /// This automatically checks for invalid combinations, such as rare s. It will instead return a normal quality + /// object in that case. + /// A valid quality-aware object, respecting the limitations of what objects can exist at non-normal quality, or + /// if was . + [return: NotNullIfNotNull(nameof(target))] + public static IObjectWithQuality? With(this T? target, Quality quality) where T : FactorioObject => ObjectWithQuality.Get(target, quality); /// - /// If possible, converts an into one with a different generic parameter. + /// Gets the with the same and the specified + /// . /// - /// The desired type parameter for the output . - /// The input to be converted. - /// If ?.target is , an with - /// the same target and quality as . Otherwise, . - /// if the conversion was successful, or if it was not. - public static bool Is(this IObjectWithQuality? obj, [NotNullWhen(true)] out IObjectWithQuality? result) where T : FactorioObject { - result = obj.As(); - return result is not null; - } + /// The corresponding quality-oblivious type. This must be or a derived type. + /// An object containing the desired , or to return + /// . + /// The desired quality. If .target does not + /// accept quality modifiers (e.g. fluids or technologies), the return value will have normal quality regardless of the value of this + /// parameter. + /// This automatically checks for invalid combinations, such as rare s. It will instead return a normal quality + /// object in that case. + /// A valid quality-aware object, respecting the limitations of what objects can exist at non-normal quality, or + /// if was . + [return: NotNullIfNotNull(nameof(target))] + public static IObjectWithQuality? With(this IObjectWithQuality? target, Quality quality) where T : FactorioObject + => ObjectWithQuality.Get(target?.target, quality); /// /// Tests whether an can be converted to one with a different generic parameter. /// /// The desired type parameter for the conversion to be tested. - /// The input to be converted. + /// The input to be tested. /// if the conversion is possible, or if it was not. - public static bool Is(this IObjectWithQuality? obj) where T : FactorioObject => obj?.target is T; - - /// - /// If possible, converts an into one with a different generic parameter. - /// - /// The desired type parameter for the output . - /// The input to be converted. - /// If ?.target is , an with - /// the same target and quality as . Otherwise, . - /// if the conversion was successful, or if it was not. - public static IObjectWithQuality? As(this IObjectWithQuality? obj) where T : FactorioObject { - if (obj is null or IObjectWithQuality) { - return obj as IObjectWithQuality; - } - // Use the conversion because it permits a null target. The constructor does not. - return (ObjectWithQuality?)(obj.target as T, obj.quality); - } + public static bool Is(this IObjectWithQuality? target) where T : FactorioObject => target?.target is T; } diff --git a/Yafc.Model/Model/RecipeParameters.cs b/Yafc.Model/Model/RecipeParameters.cs index b04ca74d..52aa1a75 100644 --- a/Yafc.Model/Model/RecipeParameters.cs +++ b/Yafc.Model/Model/RecipeParameters.cs @@ -31,8 +31,8 @@ public enum WarningFlags { } public struct UsedModule { - public (ObjectWithQuality module, int count, bool beacon)[]? modules; - public ObjectWithQuality? beacon; + public (IObjectWithQuality module, int count, bool beacon)[]? modules; + public IObjectWithQuality? beacon; public int beaconCount; } @@ -50,9 +50,9 @@ internal class RecipeParameters(float recipeTime, float fuelUsagePerSecondPerBui public static RecipeParameters CalculateParameters(IRecipeRow row) { WarningFlags warningFlags = 0; - ObjectWithQuality? entity = row.entity; - ObjectWithQuality? recipe = row.RecipeRow?.recipe; - ObjectWithQuality? fuel = row.fuel; + IObjectWithQuality? entity = row.entity; + IObjectWithQuality? recipe = row.RecipeRow?.recipe; + IObjectWithQuality? fuel = row.fuel; float recipeTime, fuelUsagePerSecondPerBuilding = 0, productivity, speed, consumption; ModuleEffects activeEffects = default; UsedModule modules = default; diff --git a/Yafc.Parser/Data/FactorioDataDeserializer.cs b/Yafc.Parser/Data/FactorioDataDeserializer.cs index 111a50fc..b992cf58 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer.cs @@ -169,6 +169,7 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes, var iconRenderTask = renderIcons ? Task.Run(RenderIcons) : Task.CompletedTask; UpdateRecipeCatalysts(); CalculateItemWeights(); + ObjectWithQuality.LoadCache(allObjects); ExportBuiltData(); progress.Report(("Post-processing", "Calculating dependencies")); Dependencies.Calculate(); diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs index 8ec0c6e4..53c78c32 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs @@ -189,13 +189,13 @@ private void ExportBuiltData() { } Database.allSciencePacks = [.. sciencePacks]; - Database.voidEnergy = new(voidEnergy, Quality.Normal); - Database.science = new(science, Quality.Normal); - Database.itemInput = new(totalItemInput, Quality.Normal); - Database.itemOutput = new(totalItemOutput, Quality.Normal); - Database.electricity = new(electricity, Quality.Normal); - Database.electricityGeneration = new(generatorProduction, Quality.Normal); - Database.heat = new(heat, Quality.Normal); + Database.voidEnergy = voidEnergy.With(Quality.Normal); + Database.science = science.With(Quality.Normal); + Database.itemInput = totalItemInput.With(Quality.Normal); + Database.itemOutput = totalItemOutput.With(Quality.Normal); + Database.electricity = electricity.With(Quality.Normal); + Database.electricityGeneration = generatorProduction.With(Quality.Normal); + Database.heat = heat.With(Quality.Normal); Database.character = character; int firstSpecial = 0; int firstItem = Skip(firstSpecial, FactorioObjectSortOrder.SpecialGoods); diff --git a/Yafc/Widgets/ImmediateWidgets.cs b/Yafc/Widgets/ImmediateWidgets.cs index 773163f2..15801e25 100644 --- a/Yafc/Widgets/ImmediateWidgets.cs +++ b/Yafc/Widgets/ImmediateWidgets.cs @@ -323,14 +323,14 @@ public static void BuildObjectSelectDropDown(this ImGui gui, ICollection l /// If not , this will be called, and the dropdown will be closed, when the user selects a quality. /// In general, set this parameter when modifying an existing object, and leave it when setting a new object. /// Width of the popup. Make sure the header text fits! - public static void BuildObjectQualitySelectDropDown(this ImGui gui, ICollection list, Action> selectItem, ObjectSelectOptions options, + public static void BuildObjectQualitySelectDropDown(this ImGui gui, ICollection list, Action> selectItem, ObjectSelectOptions options, Quality quality, Action? selectQuality = null, float width = 20f) where T : FactorioObject => gui.ShowDropDown(gui => { if (gui.BuildQualityList(quality, out quality) && selectQuality != null && gui.CloseDropdown()) { selectQuality(quality); } - gui.BuildInlineObjectListAndButton(list, i => selectItem(new(i, quality)), options); + gui.BuildInlineObjectListAndButton(list, i => selectItem(i.With(quality)), options); }, width); /// Shows a dropdown containing the (partial) of elements, with an action for when an element is selected. @@ -343,14 +343,14 @@ public static void BuildObjectSelectDropDownWithNone(this ImGui gui, ICollect /// Also shows the available quality levels, and allows the user to select a quality. /// This will be called, and the dropdown will be closed, when the user selects a quality. /// Width of the popup. Make sure the header text fits! - public static void BuildObjectQualitySelectDropDownWithNone(this ImGui gui, ICollection list, Action?> selectItem, ObjectSelectOptions options, + public static void BuildObjectQualitySelectDropDownWithNone(this ImGui gui, ICollection list, Action?> selectItem, ObjectSelectOptions options, Quality quality, Action selectQuality, float width = 20f) where T : FactorioObject => gui.ShowDropDown(gui => { if (gui.BuildQualityList(quality, out quality) && gui.CloseDropdown()) { selectQuality(quality); } - gui.BuildInlineObjectListAndButtonWithNone(list, i => selectItem((i, quality)), options); + gui.BuildInlineObjectListAndButtonWithNone(list, i => selectItem(i.With(quality)), options); }, width); /// Draws a button displaying the icon belonging to a , or an empty box as a placeholder if no object is available. diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index 1a4dbbbd..2a878b01 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -276,7 +276,7 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { using (gui.EnterGroup(contentPadding)) { if (entity.spoilResult != null) { gui.BuildText($"After {DataUtils.FormatTime(spoilTime)} of no production, spoils into"); - gui.BuildFactorioObjectButtonWithText(new ObjectWithQuality(entity.spoilResult, quality), iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); + gui.BuildFactorioObjectButtonWithText(entity.spoilResult.With(quality), iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); } else { gui.BuildText($"Expires after {DataUtils.FormatTime(spoilTime)} of no production"); diff --git a/Yafc/Windows/ProjectPageSettingsPanel.cs b/Yafc/Windows/ProjectPageSettingsPanel.cs index e9080cb7..d4e30c76 100644 --- a/Yafc/Windows/ProjectPageSettingsPanel.cs +++ b/Yafc/Windows/ProjectPageSettingsPanel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; using System.Linq; @@ -165,12 +166,12 @@ private class ExportRecipe { public ExportRecipe(RecipeRow row) { Recipe = row.recipe.QualityName(); - Building = row.entity; + Building = ObjectWithQuality.Get(row.entity); BuildingCount = row.buildingCount; Fuel = new ExportMaterial(row.fuel?.QualityName() ?? "", row.FuelInformation.Amount); Inputs = row.Ingredients.Select(i => new ExportMaterial(i.Goods?.QualityName() ?? "Recipe disabled", i.Amount)); Outputs = row.Products.Select(p => new ExportMaterial(p.Goods?.QualityName() ?? "Recipe disabled", p.Amount)); - Beacon = row.usedModules.beacon; + Beacon = ObjectWithQuality.Get(row.usedModules.beacon); BeaconCount = row.usedModules.beaconCount; if (row.usedModules.modules is null) { @@ -182,10 +183,10 @@ public ExportRecipe(RecipeRow row) { foreach (var (module, count, isBeacon) in row.usedModules.modules) { if (isBeacon) { - beaconModules.AddRange(Enumerable.Repeat(module, count)); + beaconModules.AddRange(Enumerable.Repeat(ObjectWithQuality.Get(module).Value, count)); } else { - modules.AddRange(Enumerable.Repeat(module, count)); + modules.AddRange(Enumerable.Repeat(ObjectWithQuality.Get(module).Value, count)); } } @@ -277,8 +278,8 @@ public static void LoadProjectPageFromClipboard() { } private record struct ObjectWithQuality(string Name, string Quality) { - public static implicit operator ObjectWithQuality?(ObjectWithQuality? value) => value == null ? default(ObjectWithQuality?) : new ObjectWithQuality(value.target.name, value.quality.name); - public static implicit operator ObjectWithQuality(ObjectWithQuality value) => new ObjectWithQuality(value.target.name, value.quality.name); - public static implicit operator ObjectWithQuality?(ObjectWithQuality? value) => value == null ? default(ObjectWithQuality?) : new ObjectWithQuality(value.target.name, value.quality.name); + [return: NotNullIfNotNull(nameof(objectWithQuality))] + public static ObjectWithQuality? Get(IObjectWithQuality? objectWithQuality) + => objectWithQuality == null ? default : new(objectWithQuality.target.name, objectWithQuality.quality.name); } } diff --git a/Yafc/Windows/SelectMultiObjectPanel.cs b/Yafc/Windows/SelectMultiObjectPanel.cs index ae87beb3..a88611ac 100644 --- a/Yafc/Windows/SelectMultiObjectPanel.cs +++ b/Yafc/Windows/SelectMultiObjectPanel.cs @@ -40,7 +40,7 @@ public static void Select(IEnumerable list, string? header, Action sele /// The string that describes to the user why they're selecting these items. /// An action to be called for each selected item when the panel is closed. /// An optional ordering specifying how to sort the displayed items. If , defaults to . - public static void SelectWithQuality(IEnumerable list, string header, Action> selectItem, Quality currentQuality, + public static void SelectWithQuality(IEnumerable list, string header, Action> selectItem, Quality currentQuality, IComparer? ordering = null, Predicate? checkMark = null, Predicate? yellowMark = null) where T : FactorioObject { SelectMultiObjectPanel panel = new(o => checkMark?.Invoke((T)o) ?? false, o => yellowMark?.Invoke((T)o) ?? false); // This casting is messy, but pushing T all the way around the call stack and type tree was messier. diff --git a/Yafc/Windows/SelectObjectPanel.cs b/Yafc/Windows/SelectObjectPanel.cs index 7c784262..bfe5e5c5 100644 --- a/Yafc/Windows/SelectObjectPanel.cs +++ b/Yafc/Windows/SelectObjectPanel.cs @@ -25,9 +25,9 @@ public abstract class SelectObjectPanel : PseudoScreenWithResult { protected SelectObjectPanel() : base(40f) => list = new SearchableList(30, new Vector2(2.5f, 2.5f), ElementDrawer, ElementFilter); - protected void SelectWithQuality(IEnumerable list, string? header, Action?> selectItem, IComparer? ordering, Action> mapResult, + protected void SelectWithQuality(IEnumerable list, string? header, Action?> selectItem, IComparer? ordering, Action> mapResult, bool allowNone, string? noneTooltip, Quality? currentQuality) where U : FactorioObject - => Select(list, header, u => selectItem((u, this.currentQuality!)), ordering, mapResult, allowNone, noneTooltip, currentQuality ?? Quality.Normal); + => Select(list, header, u => selectItem(u.With(this.currentQuality!)), ordering, mapResult, allowNone, noneTooltip, currentQuality ?? Quality.Normal); /// /// Opens a to allow the user to select zero or more s. diff --git a/Yafc/Windows/SelectSingleObjectPanel.cs b/Yafc/Windows/SelectSingleObjectPanel.cs index 1638f86c..704bd449 100644 --- a/Yafc/Windows/SelectSingleObjectPanel.cs +++ b/Yafc/Windows/SelectSingleObjectPanel.cs @@ -32,7 +32,7 @@ public static void Select(IEnumerable list, string? header, Action sele /// An action to be called for the selected item when the panel is closed. /// The parameter will be if the "none" or "clear" option is selected. /// An optional ordering specifying how to sort the displayed items. If , defaults to . - public static void SelectWithQuality(IEnumerable list, string header, Action> selectItem, Quality? currentQuality, + public static void SelectWithQuality(IEnumerable list, string header, Action> selectItem, Quality? currentQuality, IComparer? ordering = null) where T : FactorioObject // null-forgiving: selectItem will not be called with null, because allowNone is false. => Instance.SelectWithQuality(list, header, selectItem!, ordering, (obj, mappedAction) => mappedAction(obj), false, null, currentQuality); @@ -60,7 +60,7 @@ public static void SelectWithNone(IEnumerable list, string? header, Action /// The parameter will be if the "none" or "clear" option is selected. /// An optional ordering specifying how to sort the displayed items. If , defaults to . /// If not , this tooltip will be displayed when hovering over the "none" item. - public static void SelectQualityWithNone(IEnumerable list, string? header, Action?> selectItem, Quality? currentQuality, IComparer? ordering = null, + public static void SelectQualityWithNone(IEnumerable list, string? header, Action?> selectItem, Quality? currentQuality, IComparer? ordering = null, string? noneTooltip = null) where T : FactorioObject => Instance.SelectWithQuality(list, header, selectItem, ordering, (obj, mappedAction) => mappedAction(obj), true, noneTooltip, currentQuality); diff --git a/Yafc/Windows/ShoppingListScreen.cs b/Yafc/Windows/ShoppingListScreen.cs index 0859ab8e..0665ac0d 100644 --- a/Yafc/Windows/ShoppingListScreen.cs +++ b/Yafc/Windows/ShoppingListScreen.cs @@ -59,7 +59,7 @@ private void RebuildData() { Dictionary, int> counts = []; foreach (RecipeRow recipe in recipes) { if (recipe.entity != null) { - ObjectWithQuality shopItem = new(recipe.entity.target.itemsToPlace?.FirstOrDefault() ?? (FactorioObject)recipe.entity.target, recipe.entity.quality); + IObjectWithQuality shopItem = (recipe.entity.target.itemsToPlace?.FirstOrDefault() ?? (FactorioObject)recipe.entity.target).With(recipe.entity.quality); _ = counts.TryGetValue(shopItem, out int prev); int builtCount = recipe.builtBuildings ?? (assumeAdequate ? MathUtils.Ceil(recipe.buildingCount) : 0); int displayCount = displayState switch { @@ -71,7 +71,7 @@ private void RebuildData() { totalHeat += recipe.entity.target.heatingPower * displayCount; counts[shopItem] = prev + displayCount; if (recipe.usedModules.modules != null) { - foreach ((ObjectWithQuality module, int moduleCount, bool beacon) in recipe.usedModules.modules) { + foreach ((IObjectWithQuality module, int moduleCount, bool beacon) in recipe.usedModules.modules) { if (!beacon) { _ = counts.TryGetValue(module, out prev); counts[module] = prev + displayCount * moduleCount; @@ -191,11 +191,11 @@ public override void Build(ImGui gui) { continue; } - if (element.Is(out IObjectWithQuality? g)) { + if (element is IObjectWithQuality g) { items.Add((g, rounded)); } - else if (element.Is(out IObjectWithQuality? e) && e.target.itemsToPlace.Length > 0) { - items.Add((new ObjectWithQuality((T)(object)e.target.itemsToPlace[0], e.quality), rounded)); + else if (element is IObjectWithQuality e && e.target.itemsToPlace.Length > 0) { + items.Add((((T)(object)e.target.itemsToPlace[0]).With(e.quality), rounded)); } } @@ -238,7 +238,7 @@ private void Decompose() { Dictionary, float> decomposeResult = []; void addDecomposition(FactorioObject obj, Quality quality, float amount) { - ObjectWithQuality key = new(obj, quality); + IObjectWithQuality key = obj.With(quality); if (!decomposeResult.TryGetValue(key, out float prev)) { decompositionQueue.Enqueue(key); } diff --git a/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs b/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs index d45ee88d..6a799cc6 100644 --- a/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs +++ b/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs @@ -9,7 +9,7 @@ namespace Yafc; public class ProductionSummaryView : ProjectPageView { private readonly DataGrid grid; private readonly FlatHierarchy flatHierarchy; - private ObjectWithQuality? filteredGoods; + private IObjectWithQuality? filteredGoods; private readonly Dictionary goodsToColumn = []; private readonly PaddingColumn padding; private readonly SummaryColumn firstColumn; @@ -148,7 +148,7 @@ protected override void ModelContentsChanged(bool visualOnly) { private class GoodsColumn(ProductionSummaryColumn column, ProductionSummaryView view) : DataColumn(4f) { public readonly ProductionSummaryColumn column = column; - public ObjectWithQuality goods { get; } = column.goods as ObjectWithQuality ?? new(column.goods.target, column.goods.quality); + public IObjectWithQuality goods { get; } = column.goods as IObjectWithQuality ?? column.goods.target.With(column.goods.quality); public override void BuildHeader(ImGui gui) { var moveHandle = gui.statePosition; @@ -200,7 +200,7 @@ public override void BuildElement(ImGui gui, ProductionSummaryEntry data) { } } - private void ApplyFilter(ObjectWithQuality goods) { + private void ApplyFilter(IObjectWithQuality goods) { var filter = filteredGoods == goods ? null : goods; filteredGoods = filter; model.group.UpdateFilter(goods, default); @@ -266,7 +266,7 @@ private void AddOrRemoveColumn(IObjectWithQuality goods) { } if (!found) { - model.columns.Add(new ProductionSummaryColumn(model, new(goods.target, goods.quality))); + model.columns.Add(new ProductionSummaryColumn(model, goods)); } } diff --git a/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs b/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs index 72afb608..85eda29d 100644 --- a/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs +++ b/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs @@ -193,7 +193,7 @@ private void SelectBeacon(ImGui gui) { } } - private Module[] GetModules(ObjectWithQuality? beacon) { + private Module[] GetModules(IObjectWithQuality? beacon) { var modules = (beacon == null && recipe is { recipe: IObjectWithQuality rec }) ? [.. Database.allModules.Where(rec.CanAcceptModule)] : Database.allModules; @@ -205,13 +205,13 @@ private Module[] GetModules(ObjectWithQuality? beacon) { return [.. modules.Where(x => filter.CanAcceptModule(x.moduleSpecification))]; } - private void DrawRecipeModules(ImGui gui, ObjectWithQuality? beacon, ref ModuleEffects effects) { + private void DrawRecipeModules(ImGui gui, IObjectWithQuality? beacon, ref ModuleEffects effects) { int remainingModules = recipe?.entity?.target.moduleSlots ?? 0; using var grid = gui.EnterInlineGrid(3f, 1f); var list = beacon != null ? modules!.beaconList : modules!.list;// null-forgiving: Both calls are from places where we know modules is not null for (int i = 0; i < list.Count; i++) { grid.Next(); - (ObjectWithQuality module, int fixedCount) = list[i]; + (IObjectWithQuality module, int fixedCount) = list[i]; DisplayAmount amount = fixedCount; switch (gui.BuildFactorioObjectWithEditableAmount(module, amount, ButtonDisplayStyle.ProductionTableUnscaled)) { case GoodsWithAmountEvent.LeftButtonClick: @@ -226,7 +226,7 @@ private void DrawRecipeModules(ImGui gui, ObjectWithQuality? beaco } gui.Rebuild(); }, new("Select module", DataUtils.FavoriteModule), list[idx].module.quality, quality => { - list[idx] = list[idx] with { module = new(list[idx].module.target, quality) }; + list[idx] = list[idx] with { module = list[idx].module.target.With(quality) }; gui.Rebuild(); }); break; diff --git a/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs b/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs index 3674b785..9b660547 100644 --- a/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs +++ b/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs @@ -55,7 +55,7 @@ private void ListDrawer(ImGui gui, KeyValuePair? defaultBeacon = beacon == null ? null : new(beacon, Quality.MaxAccessible); - ObjectWithQuality? beaconFillerModule = (defaultBeaconModule, Quality.MaxAccessible); + IObjectWithQuality? defaultBeacon = beacon.With(Quality.MaxAccessible); + IObjectWithQuality? beaconFillerModule = defaultBeaconModule.With(Quality.MaxAccessible); BuildHeader(gui, "Module autofill parameters"); BuildSimple(gui, modules); diff --git a/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs b/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs index d0304bd6..9e83257b 100644 --- a/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs +++ b/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs @@ -158,7 +158,7 @@ private void BuildFlow(ImGui gui, List<(RecipeRow row, float flow)> list, float if (gui.BuildFactorioObjectButtonWithText(row.recipe, amount, tooltipOptions: DrawParentRecipes(row.owner, "recipe")) == Click.Left) { // Find the corresponding links associated with the clicked recipe, IEnumerable> goods = (isLinkOutput ? row.Products.Select(p => p.Goods) : row.Ingredients.Select(i => i.Goods))!; - Dictionary, ProductionLink> links = goods + Dictionary, ProductionLink> links = goods .Select(goods => { row.FindLink(goods, out IProductionLink? link); return link as ProductionLink; }) .WhereNotNull() .ToDictionary(x => x.goods); @@ -182,13 +182,13 @@ void drawLinks(ImGui gui) { } else { IComparer> comparer = DataUtils.DefaultOrdering; - if (isLinkOutput && row.recipe.mainProduct().Is(out var mainProduct)) { + if (isLinkOutput && row.recipe.mainProduct() is IObjectWithQuality mainProduct) { comparer = DataUtils.CustomFirstItemComparer(mainProduct, comparer); } string header = isLinkOutput ? "Select product link to inspect" : "Select ingredient link to inspect"; - ObjectSelectOptions> options = new(header, comparer, int.MaxValue); - if (gui.BuildInlineObjectList(links.Keys, out ObjectWithQuality? selected, options) && gui.CloseDropdown()) { + ObjectSelectOptions> options = new(header, comparer, int.MaxValue); + if (gui.BuildInlineObjectList(links.Keys, out IObjectWithQuality? selected, options) && gui.CloseDropdown()) { changeLinkView(links[selected]); } } diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index db1bf9ee..4668b2aa 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -249,10 +249,10 @@ public override void BuildMenu(ImGui gui) { foreach (var quality in Database.qualities.all) { foreach (var product in recipe.products) { - view.CreateLink(view.model, new ObjectWithQuality(product.goods, quality)); + view.CreateLink(view.model, product.goods.With(quality)); } - view.model.AddRecipe(new(recipe, quality), DefaultVariantOrdering); + view.model.AddRecipe(recipe.With(quality), DefaultVariantOrdering); } goodsHaveNoProduction:; } @@ -267,7 +267,7 @@ private static void BuildRecipeButton(ImGui gui, ProductionTable table) { if (gui.BuildButton("Add raw recipe").WithTooltip(gui, "Ctrl-click to add a technology instead") && gui.CloseDropdown()) { if (InputSystem.Instance.control) { SelectMultiObjectPanel.Select(Database.technologies.all, "Select technology", - r => table.AddRecipe(new(r, Quality.Normal), DefaultVariantOrdering), checkMark: r => table.recipes.Any(rr => rr.recipe.target == r), yellowMark: r => table.GetAllRecipes().Any(rr => rr.recipe.target == r)); + r => table.AddRecipe(r.With(Quality.Normal), DefaultVariantOrdering), checkMark: r => table.recipes.Any(rr => rr.recipe.target == r), yellowMark: r => table.GetAllRecipes().Any(rr => rr.recipe.target == r)); } else { var prodTable = ProductionLinkSummaryScreen.FindProductionTable(table, out List parents); @@ -350,7 +350,7 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { if (favoriteCrafter != null && recipe.entity?.target != favoriteCrafter) { _ = recipe.RecordUndo(); - recipe.entity = new(favoriteCrafter, recipe.entity?.quality ?? Quality.MaxAccessible); + recipe.entity = favoriteCrafter.With(recipe.entity?.quality ?? Quality.MaxAccessible); if (!recipe.entity.target.energy.fuels.Contains(recipe.fuel?.target)) { recipe.fuel = recipe.entity.target.energy.fuels.AutoSelect(DataUtils.FavoriteFuel).With(Quality.Normal); @@ -377,7 +377,7 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { view.BuildGoodsIcon(gui, fuel, fuelLink, fuelAmount, ProductDropdownType.Fuel, recipe, recipe.linkRoot, HintLocations.OnProducingRecipes); } else { - if (recipe.recipe == Database.electricityGeneration.As() && recipe.entity.target.factorioType is "solar-panel" or "lightning-attractor") { + if (recipe.recipe == Database.electricityGeneration && recipe.entity.target.factorioType is "solar-panel" or "lightning-attractor") { BuildAccumulatorView(gui, recipe); } } @@ -391,7 +391,7 @@ private static void BuildAccumulatorView(ImGui gui, RecipeRow recipe) { float requiredMj = recipe.entity?.GetCraftingSpeed() * recipe.buildingCount * (70 / 0.7f) ?? 0; // 70 seconds of charge time to last through the night requiredAccumulators = requiredMj / accumulator.AccumulatorCapacity(accumulatorQuality); } - else if (recipe.entity.Is(out IObjectWithQuality? attractor)) { + else if (recipe.entity is IObjectWithQuality attractor) { // Model the storm as rising from 0% to 100% over 30 seconds, staying at 100% for 24 seconds, and decaying over 30 seconds. // I adjusted these until the right answers came out of my Excel model. // TODO(multi-planet): Adjust these numbers based on day length. @@ -434,7 +434,7 @@ private static void BuildAccumulatorView(ImGui gui, RecipeRow recipe) { requiredChargeMw / accumulator.Power(accumulatorQuality)); } - ObjectWithQuality accumulatorWithQuality = new(accumulator, accumulatorQuality); + IObjectWithQuality accumulatorWithQuality = accumulator.With(accumulatorQuality); if (gui.BuildFactorioObjectWithAmount(accumulatorWithQuality, requiredAccumulators, ButtonDisplayStyle.ProductionTableUnscaled) == Click.Left) { ShowAccumulatorDropdown(gui, recipe, accumulator, accumulatorQuality); } @@ -470,7 +470,7 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { } _ = recipe.RecordUndo(); - recipe.entity = new(sel, quality); + recipe.entity = sel.With(quality); if (!sel.energy.fuels.Contains(recipe.fuel?.target)) { recipe.fuel = recipe.entity.target.energy.fuels.AutoSelect(DataUtils.FavoriteFuel).With(Quality.Normal); } @@ -580,7 +580,7 @@ public override void BuildMenu(ImGui gui) { foreach (var recipe in view.GetRecipesRecursive()) { if (recipe.recipe.target.crafters.Contains(set)) { _ = recipe.RecordUndo(); - recipe.entity = new(set, recipe.entity?.quality ?? Quality.Normal); + recipe.entity = set.With(recipe.entity?.quality ?? Quality.Normal); if (!set.energy.fuels.Contains(recipe.fuel?.target)) { recipe.fuel = recipe.entity.target.energy.fuels.AutoSelect(DataUtils.FavoriteFuel).With(Quality.Normal); @@ -783,7 +783,7 @@ private void ShowModuleDropDown(ImGui gui, RecipeRow recipe) { else { _ = dropGui.BuildQualityList(quality, out quality); } - dropGui.BuildInlineObjectListAndButton(modules, m => recipe.SetFixedModule(new(m, quality)), new("Select fixed module", DataUtils.FavoriteModule)); + dropGui.BuildInlineObjectListAndButton(modules, m => recipe.SetFixedModule(m.With(quality)), new("Select fixed module", DataUtils.FavoriteModule)); } if (moduleTemplateList.data.Count > 0) { @@ -846,7 +846,7 @@ private void CreateLink(ProductionTable table, IObjectWithQuality goods) return; } - ProductionLink link = new ProductionLink(table, new(goods.target, goods.quality)); + ProductionLink link = new ProductionLink(table, goods.target.With(goods.quality)); Rebuild(); table.RecordUndo().links.Add(link); } @@ -858,7 +858,7 @@ private void DestroyLink(ProductionLink link) { } } - private static void CreateNewProductionTable(ObjectWithQuality goods, float amount) { + private static void CreateNewProductionTable(IObjectWithQuality goods, float amount) { var page = MainScreen.Instance.AddProjectPage(goods.target.locName, goods.target, typeof(ProductionTable), true, false); ProductionTable content = (ProductionTable)page.content; ProductionLink link = new ProductionLink(content, goods) { amount = amount > 0 ? amount : 1 }; @@ -866,7 +866,7 @@ private static void CreateNewProductionTable(ObjectWithQuality goods, flo content.RebuildLinkMap(); } - private void OpenProductDropdown(ImGui targetGui, Rect rect, ObjectWithQuality goods, float amount, IProductionLink? iLink, + private void OpenProductDropdown(ImGui targetGui, Rect rect, IObjectWithQuality goods, float amount, IProductionLink? iLink, ProductDropdownType type, RecipeRow? recipe, ProductionTable context, Goods[]? variants = null) { if (InputSystem.Instance.shift) { @@ -883,18 +883,18 @@ private void OpenProductDropdown(ImGui targetGui, Rect rect, ObjectWithQuality curLevelRecipes.Contains(rec.With(goods.quality)); bool recipeExistsAnywhere(RecipeOrTechnology rec) => allRecipes.Contains(rec.With(goods.quality)); - ObjectWithQuality? selectedFuel = null; - ObjectWithQuality? spentFuel = null; + IObjectWithQuality? selectedFuel = null; + IObjectWithQuality? spentFuel = null; async void addRecipe(RecipeOrTechnology rec) { - ObjectWithQuality qualityRecipe = rec.With(goods.quality); + IObjectWithQuality qualityRecipe = rec.With(goods.quality); if (variants == null) { CreateLink(context, goods); } else { foreach (var variant in variants) { if (rec.GetProductionPerRecipe(variant) > 0f) { - CreateLink(context, new ObjectWithQuality(variant, goods.quality)); + CreateLink(context, variant.With(goods.quality)); if (variant != goods.target) { // null-forgiving: If variants is not null, neither is recipe: Only the call from BuildGoodsIcon sets variants, @@ -987,10 +987,10 @@ void dropDownContent(ImGui gui) { #region Recipe selection int numberOfShownRecipes = 0; - if (goods == Database.science.As()) { + if (goods == Database.science) { if (gui.BuildButton("Add technology") && gui.CloseDropdown()) { SelectMultiObjectPanel.Select(Database.technologies.all, "Select technology", - r => context.AddRecipe(new(r, Quality.Normal), DefaultVariantOrdering), checkMark: r => context.recipes.Any(rr => rr.recipe.target == r)); + r => context.AddRecipe(r.With(Quality.Normal), DefaultVariantOrdering), checkMark: r => context.recipes.Any(rr => rr.recipe.target == r)); } } else if (type <= ProductDropdownType.Ingredient && allProduction.Length > 0) { @@ -1318,7 +1318,7 @@ private void BuildGoodsIcon(ImGui gui, IObjectWithQuality? goods, IProduc switch (evt) { case GoodsWithAmountEvent.LeftButtonClick when goods is not null: - OpenProductDropdown(gui, gui.lastRect, new(goods.target, goods.quality), amount, link, dropdownType, recipe, context, variants); + OpenProductDropdown(gui, gui.lastRect, goods, amount, link, dropdownType, recipe, context, variants); break; case GoodsWithAmountEvent.RightButtonClick when goods is not null and { target.isLinkable: true } && (link is not ProductionLink || link.owner != context): CreateLink(context, goods); @@ -1586,7 +1586,7 @@ private static void AddDesiredProductAtLevel(ProductionTable table) => SelectMul link.RecordUndo().amount = 1f; } else { - table.RecordUndo().links.Add(new ProductionLink(table, new(product.target, product.quality)) { amount = 1f }); + table.RecordUndo().links.Add(new ProductionLink(table, product.target.With(product.quality)) { amount = 1f }); } }, Quality.Normal); diff --git a/changelog.txt b/changelog.txt index b90e5b6e..6970581b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,6 +15,11 @@ // Internal changes: // Changes to the code that do not affect the behavior of the program. ---------------------------------------------------------------------------------------------------------------------- +Version: +Date: + Internal changes: + - Quality objects now have reference equality, like FactorioObjects. +---------------------------------------------------------------------------------------------------------------------- Version: 2.11.0 Date: March 21st 2025 Features: From 7ef7d1c34e4b54f6008830fe72962c18286c0c95 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 24 Feb 2025 02:51:27 -0500 Subject: [PATCH 04/54] fix: Restore serialization of objects with quality. This now includes serialization as dictionary keys, as has always been supported for FactorioObjects. --- Yafc.Model/Serialization/ValueSerializers.cs | 116 +++++++++++++++++++ changelog.txt | 2 +- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/Yafc.Model/Serialization/ValueSerializers.cs b/Yafc.Model/Serialization/ValueSerializers.cs index 3497ab33..908fcab9 100644 --- a/Yafc.Model/Serialization/ValueSerializers.cs +++ b/Yafc.Model/Serialization/ValueSerializers.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text.Json; +using System.Text.RegularExpressions; namespace Yafc.Model; @@ -21,6 +22,10 @@ public static bool IsValueSerializerSupported(Type type) { return true; } + if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IObjectWithQuality<>)) { + return true; + } + if (type.IsClass && (typeof(ModelObject).IsAssignableFrom(type) || type.GetCustomAttribute() != null)) { return true; } @@ -75,6 +80,10 @@ private static object CreateValueSerializer() { return Activator.CreateInstance(typeof(FactorioObjectSerializer<>).MakeGenericType(typeof(T)))!; } + if (typeof(T).IsInterface && typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(IObjectWithQuality<>)) { + return Activator.CreateInstance(typeof(QualityObjectSerializer<>).MakeGenericType(typeof(T).GenericTypeArguments[0]))!; + } + if (typeof(T).IsEnum && typeof(T).GetEnumUnderlyingType() == typeof(int)) { return Activator.CreateInstance(typeof(EnumSerializer<>).MakeGenericType(typeof(T)))!; } @@ -334,6 +343,113 @@ public override void WriteToJson(Utf8JsonWriter writer, T? value) { public override bool CanBeNull => true; } +internal sealed partial class QualityObjectSerializer : ValueSerializer> where T : FactorioObject { + private static readonly ValueSerializer objectSerializer = ValueSerializer.Default; + private static readonly ValueSerializer qualitySerializer = ValueSerializer.Default; + + public override bool CanBeNull => true; + + public override IObjectWithQuality? ReadFromJson(ref Utf8JsonReader reader, DeserializationContext context, object? owner) { + if (reader.TokenType is JsonTokenType.String or JsonTokenType.PropertyName) { + string value = reader.GetString()!; + if (value.StartsWith('!')) { + // Read the new dictionary-key format, typically "!Item.coal!normal" + string separator = Separator().Match(value).Value; + string[] parts = value.Split(separator); + if (parts.Length == 3) { // The parts are , target.typeDotName, and quality.name + _ = Database.objectsByTypeName.TryGetValue(parts[1], out var obj); + _ = Database.objectsByTypeName.TryGetValue("Quality." + parts[2], out var qual); + if (obj is not T) { + context.Error($"Factorio object '{parts[1]}' no longer exists. Check mods configuration.", ErrorSeverity.MinorDataLoss); + } + else if (qual is not Quality quality) { + context.Error($"Factorio quality '{parts[2]}' no longer exists. Check mods configuration.", ErrorSeverity.MinorDataLoss); + } + else { + return ObjectWithQuality.Get(obj as T, quality); + } + } + // If anything went wrong reading the dictionary-key format, try the non-quality format instead. + } + + // Read the old non-quality "Item.coal" format + return ObjectWithQuality.Get(objectSerializer.ReadFromJson(ref reader, context, owner), Quality.Normal); + } + + if (reader.TokenType == JsonTokenType.StartObject) { + // Read the object-based quality format: { "target": "Item.coal", "quality": "Quality.normal" } + reader.Read(); // { + reader.Read(); // "target": + T? obj = objectSerializer.ReadFromJson(ref reader, context, owner); + reader.Read(); // target + reader.Read(); // "quality": + Quality? quality = qualitySerializer.ReadFromJson(ref reader, context, owner); + reader.Read(); // quality + // If anything went wrong, the error has already been reported. We can just return null and continue. + return quality == null ? null : ObjectWithQuality.Get(obj, quality); + } + + if (reader.TokenType != JsonTokenType.Null) { + context.Error($"Unexpected token {reader.TokenType} reading a quality object.", ErrorSeverity.MinorDataLoss); + } + + return null; // Input is JsonTokenType.Null, or couldn't parse the input + } + + public override void WriteToJson(Utf8JsonWriter writer, IObjectWithQuality? value) { + if (value == null) { + writer.WriteNullValue(); + return; + } + + // TODO: This writes values that can be read by older versions of Yafc. For consistency with quality objects written as dictionary keys, + // replace these four lines with this after a suitable period of backwards compatibility: + // writer.WriteStringValue(GetJsonProperty(value)); + writer.WriteStartObject(); + writer.WriteString("target", value.target.typeDotName); + writer.WriteString("quality", value.quality.typeDotName); + writer.WriteEndObject(); + } + + public override string GetJsonProperty(IObjectWithQuality value) { + string target = value.target.typeDotName; + string quality = value.quality.name; + + // Construct an unambiguous separator that does not appear in the string data: Alternate ! and @ until (A) the separator does not appear + // in either the target or quality names, and (B) the first character of the object name cannot be interpreted as part of the separator. + // That is, if target is (somehow) "!target", separator cannot be "!@", because there would otherwise be no way to distinguish + // "!@" + "!target" from "!@!" + "target". + // Note that "!@!" + "@quality" is fine. Once we see that the initial "!@!" is not followed by '@', we know the separator is "!@!", and + // the @ in "@quality" must be part of the quality name. + // In reality, rule B should never trigger, since types do not start with ! or @. + // Even rule A will be rare, since most internal names do not contain '!'. + string separator = "!"; + while (true) { + if (target.Contains(separator) || quality.Contains(separator) || target.StartsWith('@')) { + separator += '@'; + } + else { + break; + } + if (target.Contains(separator) || quality.Contains(separator) || target.StartsWith('!')) { + separator += '!'; + } + else { + break; + } + } + + return separator + target + separator + quality; + } + + public override IObjectWithQuality? ReadFromUndoSnapshot(UndoSnapshotReader reader, object owner) + => (IObjectWithQuality?)reader.ReadManagedReference(); + public override void WriteToUndoSnapshot(UndoSnapshotBuilder writer, IObjectWithQuality? value) => writer.WriteManagedReference(value); + + [GeneratedRegex("^(!@)*!?")] + private static partial Regex Separator(); +} + internal class EnumSerializer : ValueSerializer where T : struct, Enum { public EnumSerializer() { if (Unsafe.SizeOf() != 4) { diff --git a/changelog.txt b/changelog.txt index 6970581b..81609883 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,7 +18,7 @@ Version: Date: Internal changes: - - Quality objects now have reference equality, like FactorioObjects. + - Quality objects now have reference equality and abstract serialization, like FactorioObjects. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.0 Date: March 21st 2025 From e9f72b096c6ac80d0e4e50096bbe1cf7645c18d2 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Wed, 12 Feb 2025 08:24:52 -0500 Subject: [PATCH 05/54] change: Clean and document the serialization system. --- Docs/Architecture/Serialization.md | 148 ++++++++++++++++++ .../SerializationTreeChangeDetection.cs | 3 + .../SerializationTypeValidation.cs | 71 +++++++-- .../Serialization/PropertySerializers.cs | 33 +--- Yafc.Model/Serialization/SerializationMap.cs | 51 +++--- Yafc.Model/Serialization/ValueSerializers.cs | 120 ++++++++++++-- 6 files changed, 351 insertions(+), 75 deletions(-) create mode 100644 Docs/Architecture/Serialization.md diff --git a/Docs/Architecture/Serialization.md b/Docs/Architecture/Serialization.md new file mode 100644 index 00000000..b44f2ad6 --- /dev/null +++ b/Docs/Architecture/Serialization.md @@ -0,0 +1,148 @@ +# Serialization system +The project serialization system is somewhat fragile. +It adapts properly to many changes, but can also fail silently. +To reduce the chances of silent failures, it has some [guardrails in the tests](#guarding-tests), to ensure any relevant changes are intentional and functional. + +The serialization system only deals with public instance properties. +Broadly, there are two kinds of public properties, which we'll call 'writable' and 'non-writable'. +A writable property has a public setter, a constructor parameter with the same name and type, or both. +Any other public property is non-writable. +`[Obsolete]` writable properties may (and usually should) omit the getter. + +All constructor parameters, except for the first parameter when derived from `ModelObject<>`, must have the same name and type as a writable property. + +## Property types +The primary data structures that get serialized and deserialized are types derived from `ModelOject` and concrete (non-abstract) classes with the `[Serializable]` attribute. +These are serialized property-by-property, recursively. +Properties are handled as described here: + +|Property type|Writable|Non-writable| +|-|-|-| +|[`ModelObject` and derived types](#modelobjects-and-serializable-types)|Supported if concrete|Supported| +|[`[Serializable]` classes](#modelobjects-and-serializable-types)|Supported if concrete|Ignored| +|[`ReadOnlyCollection<>` and types that implement `ICollection<>` or `IDictionary<,>`](#collections)|Error|Supported if content is supported| +|[`FactorioOject`, all derived types, and `IObjectWithQuality<>`s](#factorioobjects-and-iobjectwithqualitys)|Supported, even when abstract|Ignored| +|[Native and native-like types](#native-types)|Supported if listed|Ignored| +|Any type, if the property has `[SkipSerialization]`|Ignored|Ignored| +|Other types|Error|Ignored| + +Notes: +* The constructor must initialize non-writable serialized properties to a non-`null` value. +The property may be declared to return any type that matches the _Property type_ column above. +* Value types are only supported if they appear in the list of [supported native types](#native-types). +* `[Obsolete]` properties must follow all the same rules, except that they do not need a getter if they are writable. + +### `ModelObject`s and `[Serializable]` types +Each class should have exactly one public constructor. +If the class has multiple public constructors, the serialization system will use the first, for whatever definition of "first" the compiler happens to use.\ +**Exception**: If the class has the `[DeserializeWithNonPublicConstructor]` attribute, it should have exactly one non-public constructor instead. + +The constructor may have any number of parameters, subject to the following limitations: +* The first constructor parameter for a type derived (directly or indirectly) from `ModelObject` does not follow the normal rules. +It must be present, must be of type `T`, and can have any name. +* Each (other) parameter must have the same type and name as one of the class's writable properties. +Parameters may match either directly owned properties or properties inherited from a base class. + +Writable properties that are not one of the supported types must have the `[SkipSerialization]` attribute. + +Non-writable serialized properties must be initialized by the constructor to a non-`null` value. + +### Collections +Collection values must be stored in non-writable properties, must not be passed to the constructor, and must be initialized to an empty collection. +Unsupported keys or values will cause the property to be silently ignored. +Arrays and other fixed-size collections (of supported values) will cause an error when deserializing, even though they implement `ICollection<>`. + +Keys (for dictionaries) must be: `FactorioObject` or derived from it, `IObjectWithQuality<>`, `string`, `Guid`, or `Type`. + +Values may be of any type that can be stored in a writable property. +Explicitly, values are allowed to contain collections, but may not themselves be collections. + +The serializer has special magic allowing it to modify a `ReadOnlyCollection<>`. +Initialize the `ReadOnlyCollection<>` to `new([])`, and the serializer will populate it correctly. + +### `FactorioObject`s and `IObjectWithQuality<>`s +When deserialized, `FactorioObject`s are set to the corresponding object from `Database.allObjects`. +If that object cannot be found (for example, if the mod defining it was removed), it will be set to `null`. +Properties may return `FactorioObject` or any derived type, even if the type is abstract. + +`IObjectWithQuality<>`s are the same, except that the object is fetched by calling `ObjectWithQuality.Get`. + +### Native types +The supported native and native-like types are `int`, `float`, `bool`, `ulong`, `string`, `Type`, `Guid`, `PageReference`, and `enum`s (if backed by `int`). +The `Nullable<>` versions of these types are also supported, where applicable. + +Any value types not listed above, including `Tuple`, `ValueTuple`, and all custom `struct`s, cannot be serialized/deserialized. + +## Guarding tests +There are some "unit" tests guarding the types that are processed by the serialization system. +These tests inspect things the serialization system is likely to encounter in the wild, and ensure that they comply with the rules given under [Property types](#property-types). +They also check for changes to the serialized data, to ensure that any changes are intentional. + +The failure messages should tell you exactly why the test is unhappy. +In general, the tests failures have the following meanings: +* If [`ModelObjects_AreSerializable`](../../Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs) fails, it thinks you violated one of [the rules](#modelobjects-and-serializable-types) in a type derived from `ModelObject`. +* If [`Serializables_AreSerializable`](../../Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs) fails, it thinks you violated one of [the rules](#modelobjects-and-serializable-types) in a `[Serializable]` type. +* If [`TreeHasNotChanged`](../../Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs) fails, you have added, changed, or removed serialized types or properties. + * Ensure the change was intentional and does not break serialization (e.g. changing between `List<>` and `ReadOnlyCollection<>`) or that you have handled the necessary conversions (e.g. for changing from `FactorioObject` to `List`). +Then update the dictionary initializer to match your changes. +If you made significant changes, you should be able to run `BuildPropertyTree` in the debugger to produce the initializer that the test expects. +* If you intended to add a new property to be serialized, but `TreeHasNotChanged` does not fail, the new property probably fell into one of the ["Ignored" categories](#property-types). +* If you intended to add a new `[Serializable]` type to be serialized, but `TreeHasNotChanged` does not fail, make sure there is a writable property of that type. + +Test failures are usually related to writable properties. +Non-writable properties that return array types will also cause test failures. + +## Handling property type changes +The simplest solution is probably introducing a new property and applying `[Obsolete]` to the old property. +The json deserializer will continue reading the corresponding values from the project file, if present, +but the undo system and the json serializer will ignore the old property. +You may (should?) remove the getter from an obsolete writable property. + +Sometimes obsoleting a property is not reasonable, as was the case for the changes from `FactorioObject` to `ObjectWithQuality<>`. +Depending on the requirements, you can either implement `ICustomJsonDeserializer` (as used by `ObjectWithQuality` in [081e9c0f](https://github.com/shpaass/yafc-ce/tree/081e9c0f6b47e155fbc82763590a70d90a64c83c/Yafc.Model/Data/DataClasses.cs#L819) and earlier), +or create a new `ValueSerializer` (as seen in the current `QualityObjectSerializer` implementation). + +## Adding new supported types +Consider workarounds such as using `List>` instead of `List>`, where +```c# +[Serializable] +public sealed class NestedList : IEnumerable /* do not implement ICollection<> or IList<> */ { + public List List { get; } = []; + public IEnumerator GetEnumerator() => List.GetEnumerator(); +} +``` + +### Writable properties and collection values +If the type is already part of Yafc, you may be able to support it simply by adding `[Serializable]` to the type in question. +If that isn't adequate, implement a new class derived from `ValueSerializer`. +The new type should be generic if the serialized type is generic or a type hierarchy. +Add checks for your newly supported type(s) in `IsValueSerializerSupported` and `CreateValueSerializer`. + +The tests should automatically update themselves for newly supported types in writable properties and collections. + +### Dictionary keys +Find the corresponding `ValueSerializer` implementation, or create a new one as described in the previous section. +Add an override for `GetJsonProperty`, to convert the value to a string. +If `ReadFromJson` does not support reading from a `JsonTokenType.PropertyName`, update it to do so, or override `ReadFromJsonProperty`. +In either case, return the desired object by reading the property name. +Add a check for the newly supported type in `IsKeySerializerSupported`. + +The tests should automatically update themselves for newly supported types in dictionary keys. + +### Non-writable properties +To add support for a new type in a non-writable property, implement a new +```c# +internal sealed class NewReadOnlyPropertySerializer(PropertyInfo property) + where TPropertyType : NewPropertyType[] where TOwner : class + : PropertySerializer(property, PropertyType.Normal, false) +``` +Include `TOtherTypes` if the newly supported type is generic. +Serialize nested objects using `ValueSerializer.Default`, to ensure that newly introduced `ValueSerializer` implementations are supported by your collection. + +If your newly supported type is an interface, add a check for the interface type in `GetInterfaceSerializer`. + +If your newly supported type is a class, find the two calls to `GetInterfaceSerializer`. +In the outer else block, add another check for your newly supported type. + +Changes to non-writable property support may require changes to `SerializationTypeValidation` in the vicinity of the calls to `AssertNoArraysOfSerializableValues`, +and will require changes to `TreeHasNotChanged` in the vicinity of the `typeof(ReadOnlyCollection<>)`, `typeof(IDictionary<,>)` and/or `typeof(ICollection<>)` tests. diff --git a/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs b/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs index 235c0de9..0fdb2d47 100644 --- a/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs +++ b/Yafc.Model.Tests/Serialization/SerializationTreeChangeDetection.cs @@ -148,6 +148,9 @@ public class SerializationTreeChangeDetection { // 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. + // + // If this test fails, and the change is unintentional or the next steps are not obvious, consult Docs/Architecture/Serialization.md, + // probably starting in the section 'Guarding Tests' or 'Handling property type changes'. 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."); diff --git a/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs index 1df18d01..8d74aa16 100644 --- a/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs +++ b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs @@ -12,6 +12,7 @@ public class SerializationTypeValidation { [Theory] [MemberData(nameof(ModelObjectTypes))] // Ensure that all concrete types derived from ModelObject obey the serialization rules. + // For details about the serialization rules and how to approach changes and test failures, see Docs/Architecture/Serialization.md. public void ModelObjects_AreSerializable(Type modelObjectType) { ConstructorInfo constructor = FindConstructor(modelObjectType); @@ -28,6 +29,8 @@ public void ModelObjects_AreSerializable(Type modelObjectType) { AssertConstructorParameters(modelObjectType, constructor.GetParameters().Skip(1)); AssertSettableProperties(modelObjectType); AssertDictionaryKeys(modelObjectType); + AssertCollectionValues(modelObjectType); + AssertNoArraysOfSerializableValues(modelObjectType); } public static TheoryData ModelObjectTypes => [.. AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) @@ -37,12 +40,15 @@ public void ModelObjects_AreSerializable(Type modelObjectType) { [MemberData(nameof(SerializableTypes))] // Ensure that all [Serializable] types in the Yafc namespace obey the serialization rules, // except compiler-generated types and those in Yafc.Blueprints. + // For details about the serialization rules and how to approach changes and test failures, see Docs/Architecture/Serialization.md. public void Serializables_AreSerializable(Type serializableType) { ConstructorInfo constructor = FindConstructor(serializableType); AssertConstructorParameters(serializableType, constructor.GetParameters()); AssertSettableProperties(serializableType); AssertDictionaryKeys(serializableType); + AssertCollectionValues(serializableType); + AssertNoArraysOfSerializableValues(serializableType); } public static TheoryData SerializableTypes => [.. AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) @@ -60,7 +66,7 @@ internal static ConstructorInfo FindConstructor(Type type) { // 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)}."); + Assert.True(constructor != null, $"Could not find the constructor for type {MakeTypeName(type)}. Consider adding or removing [DeserializeWithNonPublicConstructor]."); return constructor; } @@ -69,7 +75,9 @@ private static void AssertConstructorParameters(Type type, IEnumerable.CreateValueSerializer did not create a serializer."); // and must have a matching property PropertyInfo property = type.GetProperty(parameter.Name); @@ -80,36 +88,81 @@ private static void AssertConstructorParameters(Type type, IEnumerable p.GetSetMethod() != null)) { + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null)) { if (property.GetCustomAttribute() == null) { - // Properties with a public setter must be IsValueSerializerSupported + // 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."); + $"The type of property {MakeTypeName(type)}.{property.Name} should be a type supported for writable properties."); + Assert.True(ValueSerializerExists(property.PropertyType, out _), + $"For the property {MakeTypeName(type)}.{property.Name}, IsValueSerializerSupported claims the type {MakeTypeName(property.PropertyType)} is supported for writable properties (also collection/dictionary values), but ValueSerializer<>.CreateValueSerializer did not create a serializer."); } } } private void AssertDictionaryKeys(Type type) { - foreach (PropertyInfo property in type.GetProperties().Where(p => p.GetSetMethod() == null)) { + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).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]) + if (ValueSerializer.IsKeySerializerSupported(iDictionary.GenericTypeArguments[0]) && ValueSerializer.IsValueSerializerSupported(iDictionary.GenericTypeArguments[1])) { - object serializer = typeof(ValueSerializer<>).MakeGenericType(iDictionary.GenericTypeArguments[0]).GetField("Default").GetValue(null); + Assert.True(ValueSerializerExists(iDictionary.GenericTypeArguments[0], out object serializer), + $"For the property {MakeTypeName(type)}.{property.Name}, IsKeySerializerSupported claims type {MakeTypeName(iDictionary.GenericTypeArguments[0])} is supported for dictionary keys, but ValueSerializer<>.CreateValueSerializer did not create a serializer."); 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."); + $"In {MakeTypeName(type)}.{property.Name}, the dictionary keys claim to be supported, but {MakeTypeName(serializer.GetType())} does not have an overridden {nameof(ValueSerializer.GetJsonProperty)} method."); + + Assert.True(ValueSerializerExists(iDictionary.GenericTypeArguments[1], out _), + $"For the property {MakeTypeName(type)}.{property.Name}, IsValueSerializerSupported claims the type {MakeTypeName(iDictionary.GenericTypeArguments[1])} is supported for dictionary values (also writable properties), but ValueSerializer<>.CreateValueSerializer did not create a serializer."); } } } } } + private static void AssertCollectionValues(Type type) { + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() == null)) { + if (property.GetCustomAttribute() == null) { + Type iDictionary = property.PropertyType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)); + Type iCollection = property.PropertyType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)); + + if (iDictionary == null && iCollection != null && ValueSerializer.IsKeySerializerSupported(iCollection.GenericTypeArguments[0])) { + Assert.True(ValueSerializerExists(iCollection.GenericTypeArguments[0], out object serializer), + $"For the property {MakeTypeName(type)}.{property.Name}, IsValueSerializerSupported claims the type {MakeTypeName(iCollection.GenericTypeArguments[0])} is supported for collection values (also writable properties), but ValueSerializer<>.CreateValueSerializer did not create a serializer."); + } + } + } + } + + private static void AssertNoArraysOfSerializableValues(Type type) { + foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() == null)) { + if (property.GetCustomAttribute() == null) { + if (property.PropertyType.IsArray && ValueSerializer.IsValueSerializerSupported(property.PropertyType.GetElementType())) { + // Public properties without a public setter must not be arrays of writable-property values. + // (and properties with a public setter typically can't implement ICollection<>, as checked by AssertSettableProperties.) + Assert.Fail($"The non-writable property {MakeTypeName(type)}.{property.Name} is an array of writable-property values, which is not supported."); + } + } + } + } + + private static bool ValueSerializerExists(Type type, out object serializer) { + try { + serializer = typeof(ValueSerializer<>).MakeGenericType(type).GetField("Default").GetValue(null); + return serializer != null; + } + catch { + serializer = null; + return false; + } + } + internal static string MakeTypeName(Type type) { if (type.IsGenericType) { if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) { diff --git a/Yafc.Model/Serialization/PropertySerializers.cs b/Yafc.Model/Serialization/PropertySerializers.cs index 38410cc1..23388d3e 100644 --- a/Yafc.Model/Serialization/PropertySerializers.cs +++ b/Yafc.Model/Serialization/PropertySerializers.cs @@ -159,37 +159,6 @@ public override void SerializeToUndoBuilder(TOwner owner, UndoSnapshotBuilder bu public override void DeserializeFromUndoBuilder(TOwner owner, UndoSnapshotReader reader) { } } -internal class ReadWriteReferenceSerializer(PropertyInfo property) : - ReadOnlyReferenceSerializer(property, PropertyType.Normal, true) - where TOwner : ModelObject where TPropertyType : ModelObject { - - private void setter(TOwner owner, TPropertyType? value) - => _setter(owner ?? throw new ArgumentNullException(nameof(owner)), value); - private new TPropertyType? getter(TOwner owner) - => _getter(owner ?? throw new ArgumentNullException(nameof(owner))); - - public override void DeserializeFromJson(TOwner owner, ref Utf8JsonReader reader, DeserializationContext context) { - if (reader.TokenType == JsonTokenType.Null) { - return; - } - - var instance = getter(owner); - - if (instance == null) { - setter(owner, SerializationMap.DeserializeFromJson(owner, ref reader, context)); - return; - } - - base.DeserializeFromJson(owner, ref reader, context); - } - - public override void SerializeToUndoBuilder(TOwner owner, UndoSnapshotBuilder builder) => - builder.WriteManagedReference(getter(owner) ?? throw new InvalidOperationException($"Cannot serialize a null value for {property.DeclaringType}.{propertyName} to the undo snapshot.")); - - public override void DeserializeFromUndoBuilder(TOwner owner, UndoSnapshotReader reader) => - setter(owner, reader.ReadOwnedReference(owner)); -} - internal sealed class CollectionSerializer(PropertyInfo property) : PropertySerializer(property, PropertyType.Normal, false) where TCollection : ICollection where TOwner : class { @@ -358,7 +327,7 @@ public override void DeserializeFromUndoBuilder(TOwner owner, UndoSnapshotReader for (int i = 0; i < count; i++) { TKey key = KeySerializer.ReadFromUndoSnapshot(reader, owner) ?? throw new InvalidOperationException($"Serialized a null key for {property}. Cannot deserialize undo entry."); - dictionary.Add(key, DictionarySerializer.ValueSerializer.ReadFromUndoSnapshot(reader, owner)); + dictionary.Add(key, ValueSerializer.ReadFromUndoSnapshot(reader, owner)); } } } diff --git a/Yafc.Model/Serialization/SerializationMap.cs b/Yafc.Model/Serialization/SerializationMap.cs index e6d81aa6..e9f08883 100644 --- a/Yafc.Model/Serialization/SerializationMap.cs +++ b/Yafc.Model/Serialization/SerializationMap.cs @@ -19,10 +19,13 @@ public class NoUndoAttribute : Attribute { } public sealed class DeserializeWithNonPublicConstructorAttribute : Attribute { } /// -/// Represents a type that can be deserialized from JSON formats that do not match its current serialization format. This typically happens when an object changes -/// between value (string/float), object, and array representations. +/// Represents a type that can be deserialized from JSON formats that do not match its current serialization format. This typically happens when +/// an object changes between value (string/float), object, and array representations. /// /// The type that is being defined with custom deserialization rules. +/// When adding new static members to this interface, also add matching instance members to +/// and . Check that +/// helper is not and call the members of this interface using helper.MemberName. internal interface ICustomJsonDeserializer { /// /// Attempts to deserialize an object from custom (aka 'old') formats. @@ -32,8 +35,10 @@ internal interface ICustomJsonDeserializer { /// If the deserialization was successful (this method returns ), this is set to the result of reading the consumed JSON tokens. /// If unsuccessful, the output value of this parameter should not be used. /// when the custom deserialization was successful, and when the default serializer should be used instead. - // This is a static method so it can be declared for types that do no have a default constructor. + // This is a static method so it can be declared for types that do not have a default constructor. static abstract bool Deserialize(ref Utf8JsonReader reader, DeserializationContext context, out T? result); + + // When adding new static members to this interface, also add them to SerializationMap's nested types; see the remarks above. } internal abstract class SerializationMap { @@ -75,6 +80,7 @@ internal static class SerializationMap where T : class { private static readonly int constructorProperties; private static readonly ulong constructorFieldMask; private static readonly ulong requiredConstructorFieldMask; + private static readonly ICustomDeserializerHelper? helper; public class SpecificSerializationMap : SerializationMap { public override void BuildUndo(object? target, UndoSnapshotBuilder builder) { @@ -138,15 +144,11 @@ private static bool GetInterfaceSerializer(Type iface, [MaybeNullWhen(false)] ou if (definition == typeof(IDictionary<,>)) { var args = iface.GetGenericArguments(); - if (ValueSerializer.IsValueSerializerSupported(args[0])) { - keyType = args[0]; - elementType = args[1]; - - if (ValueSerializer.IsValueSerializerSupported(elementType)) { - serializerType = typeof(DictionarySerializer<,,,>); - - return true; - } + keyType = args[0]; + elementType = args[1]; + if (ValueSerializer.IsKeySerializerSupported(keyType) && ValueSerializer.IsValueSerializerSupported(elementType)) { + serializerType = typeof(DictionarySerializer<,,,>); + return true; } } } @@ -157,6 +159,7 @@ private static bool GetInterfaceSerializer(Type iface, [MaybeNullWhen(false)] ou } static SerializationMap() { + List> list = []; bool isModel = typeof(ModelObject).IsAssignableFrom(typeof(T)); @@ -167,6 +170,13 @@ static SerializationMap() { constructor = typeof(T).GetConstructors(BindingFlags.Public | BindingFlags.Instance)[0]; } + if (typeof(ICustomJsonDeserializer).IsAssignableFrom(typeof(T))) { + // I want to call methods by using, e.g. `((ICustomJsonDeserializer)T).Deserialize(...)`, but that isn't supported. + // This helper calls ICustomJsonDeserializer.Deserialize (and any new methods) by the least obscure method I could come up with. + + helper = (ICustomDeserializerHelper)Activator.CreateInstance(typeof(CustomDeserializerHelper<>).MakeGenericType(typeof(T), typeof(T)))!; + } + var constructorParameters = constructor.GetParameters(); List processedProperties = []; @@ -219,9 +229,6 @@ static SerializationMap() { if (ValueSerializer.IsValueSerializerSupported(propertyType)) { serializerType = typeof(ValuePropertySerializer<,>); } - else if (typeof(ModelObject).IsAssignableFrom(propertyType)) { - serializerType = typeof(ReadWriteReferenceSerializer<,>); - } else { throw new NotSupportedException("Type " + typeof(T) + " has property " + property.Name + " that cannot be serialized"); } @@ -292,10 +299,19 @@ public static void SerializeToJson(T? value, Utf8JsonWriter writer) { return null; } + /// + /// A helper type for calling static members of . Static members there should have matching instance + /// members here. + /// private interface ICustomDeserializerHelper { bool Deserialize(ref Utf8JsonReader reader, DeserializationContext context, out T? deserializedObject); } + /// + /// A helper type for calling static members in . Static members there should have matching instance + /// members here, which call .MemberName and return the result, casting as appropriate. + /// + /// Always the same as , though the compiler doesn't know this. private class CustomDeserializerHelper : ICustomDeserializerHelper where U : ICustomJsonDeserializer { public bool Deserialize(ref Utf8JsonReader reader, DeserializationContext context, out T? deserializedObject) { bool result = U.Deserialize(ref reader, context, out U? intermediate); @@ -309,10 +325,7 @@ public bool Deserialize(ref Utf8JsonReader reader, DeserializationContext contex return null; } - if (typeof(ICustomJsonDeserializer).IsAssignableFrom(typeof(T))) { - // I want to do `((ICustomJsonDeserializer)T).Deserialize(...)`, but that isn't supported. - // This and its helper types call ICustomJsonDeserializer.Deserialize by the least obscure method I could come up with. - ICustomDeserializerHelper helper = (ICustomDeserializerHelper)Activator.CreateInstance(typeof(CustomDeserializerHelper<>).MakeGenericType(typeof(T), typeof(T)))!; + if (helper != null) { Utf8JsonReader savedState = reader; if (helper.Deserialize(ref reader, context, out T? result)) { // The custom deserializer was successful; return its result. diff --git a/Yafc.Model/Serialization/ValueSerializers.cs b/Yafc.Model/Serialization/ValueSerializers.cs index 908fcab9..5a88a3ca 100644 --- a/Yafc.Model/Serialization/ValueSerializers.cs +++ b/Yafc.Model/Serialization/ValueSerializers.cs @@ -8,6 +8,8 @@ namespace Yafc.Model; internal static class ValueSerializer { public static bool IsValueSerializerSupported(Type type) { + // Types listed in this method must have a corresponding ValueSerializer returned from CreateValueSerializer. + if (type == typeof(int) || type == typeof(float) || type == typeof(bool) || type == typeof(ulong) || type == typeof(string) || type == typeof(Type) || type == typeof(Guid) || type == typeof(PageReference)) { @@ -22,7 +24,7 @@ public static bool IsValueSerializerSupported(Type type) { return true; } - if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IObjectWithQuality<>)) { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IObjectWithQuality<>)) { return true; } @@ -30,18 +32,46 @@ public static bool IsValueSerializerSupported(Type type) { return true; } - if (!type.IsClass && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { return IsValueSerializerSupported(type.GetGenericArguments()[0]); } return false; } + + public static bool IsKeySerializerSupported(Type type) { + // Types listed in this method must have a ValueSerializer where GetJsonProperty is overridden. + + if (type == typeof(string) || type == typeof(Type) || type == typeof(Guid)) { + return true; + } + + if (typeof(FactorioObject).IsAssignableFrom(type)) { + return true; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IObjectWithQuality<>)) { + return true; + } + + return false; + } } +/// +/// The base class for serializing property values that are [Serializable], native +/// (e.g. , ), or native-like (e.g. ). +/// +/// The type to be serialized/deserialized by this instance. internal abstract class ValueSerializer { + /// + /// Contains the serializer that should be used for . + /// public static readonly ValueSerializer Default = (ValueSerializer)CreateValueSerializer(); private static object CreateValueSerializer() { + // Types listed in this method must also return true from IsValueSerializerSupported. + if (typeof(T) == typeof(int)) { return new IntSerializer(); } @@ -76,11 +106,14 @@ private static object CreateValueSerializer() { // null-forgiving: Activator.CreateInstance only returns null for Nullable. // See System.Private.CoreLib\src\System\Activator.cs:20, e.g. https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Activator.cs#L20 + if (typeof(FactorioObject).IsAssignableFrom(typeof(T))) { + // `return new FactorioObjectSerializer();`, but in a format that works with the limitations of C#. + // The following blocks also create newly-constructed values with more restrictive constraints on T. return Activator.CreateInstance(typeof(FactorioObjectSerializer<>).MakeGenericType(typeof(T)))!; } - if (typeof(T).IsInterface && typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(IObjectWithQuality<>)) { + if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(IObjectWithQuality<>)) { return Activator.CreateInstance(typeof(QualityObjectSerializer<>).MakeGenericType(typeof(T).GenericTypeArguments[0]))!; } @@ -95,21 +128,67 @@ private static object CreateValueSerializer() { return Activator.CreateInstance(typeof(PlainClassesSerializer<>).MakeGenericType(typeof(T)))!; } - if (!typeof(T).IsClass && typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>)) { + if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>)) { return Activator.CreateInstance(typeof(NullableSerializer<>).MakeGenericType(typeof(T).GetGenericArguments()[0]))!; } throw new InvalidOperationException($"No known serializer for {typeof(T)}."); } + /// + /// Reads an object from a project file. + /// + /// The that is reading the project file. On entry, this will point to the first token to + /// be read. On exit, this should point to the last token read. + /// The active , for reporting errors in the json data. + /// The object that owns this value. This is ignored except when reading s. + /// The object read from the file, or if . + /// == . public abstract T? ReadFromJson(ref Utf8JsonReader reader, DeserializationContext context, object? owner); + + /// + /// Writes the specified value to a project file. + /// + /// The that is writing to the project file + /// The value to be written to . public abstract void WriteToJson(Utf8JsonWriter writer, T? value); + /// + /// This converts from a value to a string, which can be used as the name of a json property. + /// Note: When overriding this method, also add a check in IsKeySerializerSupported, to allow the type to be used as a dictionary key. + /// + /// The value to convert to a string + /// , converted to a string that can read. public virtual string GetJsonProperty(T value) => throw new NotSupportedException("Using type " + typeof(T) + " as dictionary key is not supported"); + /// + /// Called to read a object from a project file when . == + /// . Instead of overriding this, it is usually better to write so it + /// can read tokens. + /// + /// The that is reading the project file. On entry, this will point to the property name + /// token to be read. On exit, this should still point to the property name token. + /// The active , for reporting errors in the json data. + /// The object that owns this value. This is ignored except when reading s. + /// The object read from the file. public virtual T? ReadFromJsonProperty(ref Utf8JsonReader reader, DeserializationContext context, object owner) => ReadFromJson(ref reader, context, owner); + /// + /// Called to read an object from an undo snapshot. The data to be read was written by . + /// + /// The that contains the objects to be read. + /// The object that owns this value. This is ignored except when reading s. + /// The object read from the snapshot. public abstract T? ReadFromUndoSnapshot(UndoSnapshotReader reader, object owner); + /// + /// Called to write an object to an undo snapshot. Write data in a format that can be read by . + /// + /// The that will store the objects. public abstract void WriteToUndoSnapshot(UndoSnapshotBuilder writer, T? value); + + /// + /// Called by other portions of the serialization system to determine whether values of type are allowed to be + /// . + /// public virtual bool CanBeNull => false; } @@ -244,10 +323,6 @@ public override void WriteToJson(Utf8JsonWriter writer, Type? value) { ArgumentNullException.ThrowIfNull(value, nameof(value)); string? name = value.FullName; - // TODO: Once no one will want to roll back to 0.7.2 or earlier, remove this if block. - if (name?.StartsWith("Yafc.") ?? false) { - name = "YAFC." + name[5..]; - } writer.WriteStringValue(name); } @@ -257,13 +332,7 @@ public override string GetJsonProperty(Type value) { throw new ArgumentException($"value must be a type that has a FullName.", nameof(value)); } - string name = value.FullName; - - // TODO: Once no one will want to roll back to 0.7.2 or earlier, remove this if block. - if (name.StartsWith("Yafc.")) { - name = "YAFC." + name[5..]; - } - return name; + return value.FullName; } public override Type? ReadFromUndoSnapshot(UndoSnapshotReader reader, object owner) => reader.ReadManagedReference() as Type; @@ -476,6 +545,27 @@ public override T ReadFromUndoSnapshot(UndoSnapshotReader reader, object owner) /// Serializes classes marked with [Serializable], except blueprint classes, s, and s. /// internal class PlainClassesSerializer : ValueSerializer where T : class { + static PlainClassesSerializer() { + // The checks in CreateValueSerializer should prevent these two from happening. + if (typeof(T).IsAssignableTo(typeof(ModelObject))) { + throw new InvalidOperationException($"PlainClassesSerializer should not be used for {typeof(T)} because it is derived from ModelObject."); + } + if (typeof(T).IsAssignableTo(typeof(FactorioObject))) { + throw new InvalidOperationException($"PlainClassesSerializer should not be used for {typeof(T)} because it is derived from FactorioObject."); + } + if (!typeof(T).FullName!.StartsWith("Yafc.")) { + // Well, probably. It's unlikely this default serialization will correctly handle types that were not created with it in mind. + throw new InvalidOperationException($"PlainClassesSerializer should not be used for {typeof(T)} because it is outside the Yafc namespace."); + } + if (typeof(T).GetCustomAttribute() == null) { + // If you want standard serialization behavior, like BeaconConfiguration, add the [Serializable] attribute to the type instead of + // explicitly listing the type in IsValueSerializerSupported. + // If you want non-standard serialization behavior, like FactorioObject, add a check for this type in CreateValueSerializer, and + // return a custom serializer. + throw new InvalidOperationException($"PlainClassesSerializer should not be used for {typeof(T)} because it does not have a [Serializable] attribute."); + } + } + private static readonly SerializationMap builder = SerializationMap.GetSerializationMap(typeof(T)); public override T? ReadFromJson(ref Utf8JsonReader reader, DeserializationContext context, object? owner) => SerializationMap.DeserializeFromJson(null, ref reader, context); From 71cd6b0c225ef1e02c99cab26c7963415cd351bb Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 8 Mar 2025 16:07:23 -0500 Subject: [PATCH 06/54] change: Add checks around optional constructor args and abstract types. --- Docs/Architecture/Serialization.md | 2 ++ .../SerializationTypeValidation.cs | 22 +++++++++++++++++++ Yafc.Model/Serialization/ValueSerializers.cs | 5 +++-- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Docs/Architecture/Serialization.md b/Docs/Architecture/Serialization.md index b44f2ad6..c6679146 100644 --- a/Docs/Architecture/Serialization.md +++ b/Docs/Architecture/Serialization.md @@ -42,6 +42,8 @@ The constructor may have any number of parameters, subject to the following limi It must be present, must be of type `T`, and can have any name. * Each (other) parameter must have the same type and name as one of the class's writable properties. Parameters may match either directly owned properties or properties inherited from a base class. +* If the parameter has a default value, that value must be `default`. +(Or an equivalent: e.g. `null` for reference types and `Nullable<>`, `0` for numeric types, and/or `new()` for value types without a explicit 0-parameter constructor.) Writable properties that are not one of the supported types must have the `[SkipSerialization]` attribute. diff --git a/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs index 8d74aa16..4f0030d1 100644 --- a/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs +++ b/Yafc.Model.Tests/Serialization/SerializationTypeValidation.cs @@ -42,6 +42,9 @@ public void ModelObjects_AreSerializable(Type modelObjectType) { // except compiler-generated types and those in Yafc.Blueprints. // For details about the serialization rules and how to approach changes and test failures, see Docs/Architecture/Serialization.md. public void Serializables_AreSerializable(Type serializableType) { + Assert.False(serializableType.IsAbstract, "[Serializable] types must not be abstract."); + Assert.False(serializableType.IsValueType, "[Serializable] types must not be structs (or record structs)."); + ConstructorInfo constructor = FindConstructor(serializableType); AssertConstructorParameters(serializableType, constructor.GetParameters()); @@ -84,9 +87,28 @@ private static void AssertConstructorParameters(Type type, IEnumerable() == null, + $"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' is a required parameter, but matches a [SkipSerialization] property."); + Assert.True(property.GetCustomAttribute() == null, + $"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' is a required parameter, but matches an [Obsolete] property."); + } + else if (parameter.ParameterType.IsValueType) { + typeof(SerializationTypeValidation).GetMethod(nameof(CheckDefaultValue), BindingFlags.Static | BindingFlags.NonPublic) + .MakeGenericMethod(parameter.ParameterType) + .Invoke(null, [type, parameter]); + } } } + private static void CheckDefaultValue(Type type, ParameterInfo parameter) { + // parameter.DefaultValue is null when complex structs (e.g. Guids) should be 0-initialized. + T defaultValue = (T)(parameter.DefaultValue ?? default(T)); + Assert.True(Equals(defaultValue, default(T)), + $"Constructor of type {MakeTypeName(type)} parameter '{parameter.Name}' is an optional parameter with a default value ({defaultValue}) that is not default({MakeTypeName(parameter.ParameterType)})."); + } + private static void AssertSettableProperties(Type type) { foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null)) { if (property.GetCustomAttribute() == null) { diff --git a/Yafc.Model/Serialization/ValueSerializers.cs b/Yafc.Model/Serialization/ValueSerializers.cs index 5a88a3ca..4605e4c4 100644 --- a/Yafc.Model/Serialization/ValueSerializers.cs +++ b/Yafc.Model/Serialization/ValueSerializers.cs @@ -28,7 +28,7 @@ public static bool IsValueSerializerSupported(Type type) { return true; } - if (type.IsClass && (typeof(ModelObject).IsAssignableFrom(type) || type.GetCustomAttribute() != null)) { + if (type.IsClass && !type.IsAbstract && (typeof(ModelObject).IsAssignableFrom(type) || type.GetCustomAttribute() != null)) { return true; } @@ -121,13 +121,14 @@ private static object CreateValueSerializer() { return Activator.CreateInstance(typeof(EnumSerializer<>).MakeGenericType(typeof(T)))!; } - if (typeof(T).IsClass) { + if (typeof(T).IsClass && !typeof(T).IsAbstract) { if (typeof(ModelObject).IsAssignableFrom(typeof(T))) { return Activator.CreateInstance(typeof(ModelObjectSerializer<>).MakeGenericType(typeof(T)))!; } return Activator.CreateInstance(typeof(PlainClassesSerializer<>).MakeGenericType(typeof(T)))!; } + if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>)) { return Activator.CreateInstance(typeof(NullableSerializer<>).MakeGenericType(typeof(T).GetGenericArguments()[0]))!; } From cc0b948c80f70c6ea4532f1fdfb55340e3550b0a Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:42:30 -0400 Subject: [PATCH 07/54] change: More comments and corrected null value restrictions --- Docs/Architecture/Serialization.md | 6 +- .../Serialization/PropertySerializers.cs | 59 ++++++++++++++++--- Yafc.Model/Serialization/ValueSerializers.cs | 4 +- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/Docs/Architecture/Serialization.md b/Docs/Architecture/Serialization.md index c6679146..e8803bde 100644 --- a/Docs/Architecture/Serialization.md +++ b/Docs/Architecture/Serialization.md @@ -27,7 +27,7 @@ Properties are handled as described here: |Other types|Error|Ignored| Notes: -* The constructor must initialize non-writable serialized properties to a non-`null` value. +* The constructor must initialize serialized collections to a non-`null` value. The property may be declared to return any type that matches the _Property type_ column above. * Value types are only supported if they appear in the list of [supported native types](#native-types). * `[Obsolete]` properties must follow all the same rules, except that they do not need a getter if they are writable. @@ -47,7 +47,9 @@ Parameters may match either directly owned properties or properties inherited fr Writable properties that are not one of the supported types must have the `[SkipSerialization]` attribute. -Non-writable serialized properties must be initialized by the constructor to a non-`null` value. +Collection properties (always non-writable) must be initialized by the constructor to a non-`null` value. +The constructor is not required to initialize non-writable `ModelObject` properties. +If it does not, the serialization system will discard non-`null` values encountered in the project file. ### Collections Collection values must be stored in non-writable properties, must not be passed to the constructor, and must be initialized to an empty collection. diff --git a/Yafc.Model/Serialization/PropertySerializers.cs b/Yafc.Model/Serialization/PropertySerializers.cs index 23388d3e..27b47f9c 100644 --- a/Yafc.Model/Serialization/PropertySerializers.cs +++ b/Yafc.Model/Serialization/PropertySerializers.cs @@ -64,6 +64,14 @@ protected TPropertyType getter(TOwner owner) { } } +/// +/// With ValueSerializer<>, serializes and deserializes all values +/// stored in writable properties. +/// +/// The type (not a ) that contains this property. +/// The declared type of this property. This type must be listed in +/// . +/// This property's . internal sealed class ValuePropertySerializer(PropertyInfo property) : PropertySerializer(property, PropertyType.Normal, true) where TOwner : class { @@ -81,13 +89,8 @@ internal sealed class ValuePropertySerializer(PropertyInf var result = _getter(owner); - if (result == null) { - if (CanBeNull) { - return default; - } - else { - throw new InvalidOperationException($"{property.DeclaringType}.{propertyName} must not return null."); - } + if (result == null && !CanBeNull) { + throw new InvalidOperationException($"{property.DeclaringType}.{propertyName} must not return null."); } return result; @@ -111,7 +114,12 @@ public override void DeserializeFromUndoBuilder(TOwner owner, UndoSnapshotReader public override bool CanBeNull => ValueSerializer.CanBeNull; } -// Serializes read-only sub-value with support of polymorphism +/// +/// With ModelObjectSerializer<>, serializes and deserializes +/// s stored in non-writable properties. +/// +/// The type ( or a derived type) that contains this property. +/// The declared type ( or a derived type) of this property. internal class ReadOnlyReferenceSerializer : PropertySerializer where TOwner : ModelObject where TPropertyType : ModelObject { @@ -159,6 +167,16 @@ public override void SerializeToUndoBuilder(TOwner owner, UndoSnapshotBuilder bu public override void DeserializeFromUndoBuilder(TOwner owner, UndoSnapshotReader reader) { } } +/// +/// With ValueSerializer<>, serializes and deserializes most collection +/// values (stored in non-writable properties). +/// +/// The type (not a ) that contains this property. +/// The declared type of this property, which implements +/// ICollection<> +/// The element type stored in the serialized collection. This type must be listed in +/// . +/// This property's . internal sealed class CollectionSerializer(PropertyInfo property) : PropertySerializer(property, PropertyType.Normal, false) where TCollection : ICollection where TOwner : class { @@ -212,6 +230,16 @@ public override void DeserializeFromUndoBuilder(TOwner owner, UndoSnapshotReader } } +/// +/// With ValueSerializer<>, serializes and deserializes +/// s (stored in non-writable properties). +/// +/// The type (not a ) that contains this property. +/// The declared type of this property, which is or derives from +/// ReadOnlyCollection<>. +/// The element type stored in the serialized collection. This type must be listed in +/// . +/// This property's . internal sealed class ReadOnlyCollectionSerializer(PropertyInfo property) : PropertySerializer(property, PropertyType.Normal, false) // This is ReadOnlyCollection, not IReadOnlyCollection, because we rely on knowing about the mutable backing storage of ReadOnlyCollection. @@ -271,6 +299,21 @@ public override void DeserializeFromUndoBuilder(TOwner owner, UndoSnapshotReader } } +/// +/// With ValueSerializer<> and +/// ValueSerializer<>, serializes and deserializes dictionary values (stored +/// in non-writable properties). +/// +/// The type (not a ) that contains this property. +/// The declared type of this property, which implements +/// IDictionary<, > +/// The type of the keys stored in the dictionary. This type must be listed in +/// and , and +/// ValueSerializer<>.Default +/// must have an overridden method. +/// The type of the values stored in the dictionary. This type must be listed in +/// . +/// This property's . internal class DictionarySerializer(PropertyInfo property) : PropertySerializer(property, PropertyType.Normal, false) where TCollection : IDictionary where TOwner : class { diff --git a/Yafc.Model/Serialization/ValueSerializers.cs b/Yafc.Model/Serialization/ValueSerializers.cs index 4605e4c4..0db578e4 100644 --- a/Yafc.Model/Serialization/ValueSerializers.cs +++ b/Yafc.Model/Serialization/ValueSerializers.cs @@ -59,8 +59,8 @@ public static bool IsKeySerializerSupported(Type type) { } /// -/// The base class for serializing property values that are [Serializable], native -/// (e.g. , ), or native-like (e.g. ). +/// The base class for serializing values that are [Serializable], native (e.g. , +/// ), or native-like (e.g. ). /// /// The type to be serialized/deserialized by this instance. internal abstract class ValueSerializer { From 1e64d37ec6ab8aa9afd7d984afc5420318c1ec09 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Wed, 26 Mar 2025 01:56:16 -0400 Subject: [PATCH 08/54] fix(#446): Unnamed projects could not be created. --- Yafc.Model.Tests/Model/ProjectTests.cs | 26 ++++++++ Yafc.Model/Model/Project.cs | 85 +++++++++++++++----------- changelog.txt | 2 + 3 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 Yafc.Model.Tests/Model/ProjectTests.cs diff --git a/Yafc.Model.Tests/Model/ProjectTests.cs b/Yafc.Model.Tests/Model/ProjectTests.cs new file mode 100644 index 00000000..91ca77a0 --- /dev/null +++ b/Yafc.Model.Tests/Model/ProjectTests.cs @@ -0,0 +1,26 @@ +using System; +using Xunit; + +namespace Yafc.Model.Tests; + +public class ProjectTests { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadFromFile_CanLoadWithEmptyString(bool useMostRecent) + => Assert.NotNull(Project.ReadFromFile("", new(), useMostRecent)); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadFromFile_CanLoadNonexistentFile(bool useMostRecent) + // Assuming that there are no files named .yafc in the current directory. + => Assert.NotNull(Project.ReadFromFile(Guid.NewGuid().ToString() + ".yafc", new(), useMostRecent)); + + [Fact] + // PerformAutoSave is expected to be a no-op in this case. + // This test may need more care and feeding if autosaving is added for nameless projects. + public void PerformAutoSave_NoThrowWhenLoadedWithEmptyString() + // No Assert in this test; the test passes if PerformAutoSave does not throw. + => Project.ReadFromFile("", new(), false).PerformAutoSave(); +} diff --git a/Yafc.Model/Model/Project.cs b/Yafc.Model/Model/Project.cs index 011be8d9..a4109507 100644 --- a/Yafc.Model/Model/Project.cs +++ b/Yafc.Model/Model/Project.cs @@ -78,47 +78,62 @@ protected internal override void ThisChanged(bool visualOnly) { } public static Project ReadFromFile(string path, ErrorCollector collector, bool useMostRecent) { - Project? project; - - var highestAutosaveIndex = 0; - - // Check whether there is an autosave that is saved at a later time than the current save. - if (useMostRecent) { - var savetime = File.GetLastWriteTimeUtc(path); - var highestAutosave = Enumerable - .Range(1, AutosaveRollingLimit) - .Select(i => new { - Path = GenerateAutosavePath(path, i), - Index = i, - LastWriteTimeUtc = File.GetLastWriteTimeUtc(GenerateAutosavePath(path, i)) - }) - .MaxBy(s => s.LastWriteTimeUtc); - - if (highestAutosave != null && highestAutosave.LastWriteTimeUtc > savetime) { - highestAutosaveIndex = highestAutosave.Index; - path = highestAutosave.Path; - } + if (string.IsNullOrWhiteSpace(path)) { + // Empty paths don't have autosaves. + useMostRecent = false; } - if (!string.IsNullOrEmpty(path) && File.Exists(path)) { - project = Read(File.ReadAllBytes(path), collector); + try { + return read(path, collector, useMostRecent); } - else { - project = new Project(); + catch when (useMostRecent) { + collector.Error("Fatal error reading the latest autosave. Loading the base file instead.", ErrorSeverity.Important); + return read(path, collector, false); } - // If an Auto Save is used to open the project we want remove the 'autosave' part so when the user - // manually saves the file next time it saves the 'main' save instead of the generated save file. - if (path != null) { - var autosaveRegex = new Regex("-autosave-[0-9].yafc$"); - path = autosaveRegex.Replace(path, ".yafc"); - } + static Project read(string path, ErrorCollector collector, bool useMostRecent) { + Project? project; + + var highestAutosaveIndex = 0; + + // Check whether there is an autosave that is saved at a later time than the current save. + if (useMostRecent) { + var savetime = File.GetLastWriteTimeUtc(path); + var highestAutosave = Enumerable + .Range(1, AutosaveRollingLimit) + .Select(i => new { + Path = GenerateAutosavePath(path, i), + Index = i, + LastWriteTimeUtc = File.GetLastWriteTimeUtc(GenerateAutosavePath(path, i)) + }) + .MaxBy(s => s.LastWriteTimeUtc); + + if (highestAutosave != null && highestAutosave.LastWriteTimeUtc > savetime) { + highestAutosaveIndex = highestAutosave.Index; + path = highestAutosave.Path; + } + } - project.attachedFileName = path; - project.lastSavedVersion = project.projectVersion; - project.autosaveIndex = highestAutosaveIndex; + if (!string.IsNullOrEmpty(path) && File.Exists(path)) { + project = Read(File.ReadAllBytes(path), collector); + } + else { + project = new Project(); + } - return project; + // If an Auto Save is used to open the project we want remove the 'autosave' part so when the user + // manually saves the file next time it saves the 'main' save instead of the generated save file. + if (path != null) { + var autosaveRegex = new Regex("-autosave-[0-9].yafc$"); + path = autosaveRegex.Replace(path, ".yafc"); + } + + project.attachedFileName = path; + project.lastSavedVersion = project.projectVersion; + project.autosaveIndex = highestAutosaveIndex; + + return project; + } } public static Project Read(byte[] bytes, ErrorCollector collector) { @@ -171,7 +186,7 @@ public void Save(Stream stream) { } public void PerformAutoSave() { - if (attachedFileName != null && lastAutoSavedVersion != projectVersion) { + if (!string.IsNullOrWhiteSpace(attachedFileName) && lastAutoSavedVersion != projectVersion) { autosaveIndex = (autosaveIndex % AutosaveRollingLimit) + 1; var fileName = GenerateAutosavePath(attachedFileName, autosaveIndex); diff --git a/changelog.txt b/changelog.txt index 81609883..7e6894a1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,8 @@ ---------------------------------------------------------------------------------------------------------------------- Version: Date: + Fixes: + - (regression) Fix opening new unnamed files. Internal changes: - Quality objects now have reference equality and abstract serialization, like FactorioObjects. ---------------------------------------------------------------------------------------------------------------------- From 75a9224b236e785b65ff57aec55348fbe9e6d944 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 24 Mar 2025 22:45:10 -0400 Subject: [PATCH 09/54] fix(net9): Fix page list in the search all dropdown. .NET 9 changes (fixes) the float->int conversion behavior on x86: https://learn.microsoft.com/en-us/dotnet/core/compatibility/jit/9.0/fp-to-integer --- Yafc.UI/ImGui/ScrollArea.cs | 1 + Yafc/Windows/MainScreen.cs | 2 +- changelog.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Yafc.UI/ImGui/ScrollArea.cs b/Yafc.UI/ImGui/ScrollArea.cs index 8fef09ae..c954317d 100644 --- a/Yafc.UI/ImGui/ScrollArea.cs +++ b/Yafc.UI/ImGui/ScrollArea.cs @@ -295,6 +295,7 @@ public class VirtualScrollList(float height, Vector2 elementSize, Virtual private int elementsPerRow; private IReadOnlyList _data = []; private readonly int maxRowsVisible = MathUtils.Ceil(height / elementSize.Y) + BufferRows + 1; + private readonly Vector2 elementSize = elementSize.X > 0 && elementSize.Y > 0 ? elementSize : throw new ArgumentException("Both element size dimensions must be positive", nameof(elementSize)); private float _spacing; public float spacing { diff --git a/Yafc/Windows/MainScreen.cs b/Yafc/Windows/MainScreen.cs index d1546274..64d28c0c 100644 --- a/Yafc/Windows/MainScreen.cs +++ b/Yafc/Windows/MainScreen.cs @@ -55,7 +55,7 @@ public MainScreen(int display, Project project) : base(default, Preferences.Inst searchGui = new ImGui(BuildSearch, new Padding(1f)) { boxShadow = RectangleBorder.Thin, boxColor = SchemeColor.Background }; Instance = this; tabBar = new MainScreenTabBar(this); - allPages = new VirtualScrollList(30, new Vector2(0f, 2f), BuildPage, collapsible: true); + allPages = new VirtualScrollList(30, new Vector2(float.PositiveInfinity, 2f), BuildPage, collapsible: true); Create("Yet Another Factorio Calculator CE v" + YafcLib.version.ToString(3), display, Preferences.Instance.initialMainScreenWidth, Preferences.Instance.initialMainScreenHeight, Preferences.Instance.maximizeMainScreen); diff --git a/changelog.txt b/changelog.txt index 7e6894a1..54e79f2d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ Version: Date: Fixes: - (regression) Fix opening new unnamed files. + - (.NET 9) Fix page name display in the search-all dropdown. Internal changes: - Quality objects now have reference equality and abstract serialization, like FactorioObjects. ---------------------------------------------------------------------------------------------------------------------- From d3077bb14f12d026b498c5582d232552e593d647 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Tue, 1 Apr 2025 01:51:02 -0400 Subject: [PATCH 10/54] fix: Inserters cannot hold more than one stack of items. --- Yafc/Workspace/ProductionTable/ProductionTableView.cs | 8 ++++---- changelog.txt | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index 4668b2aa..43260908 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -1193,8 +1193,8 @@ void dropDownContent(ImGui gui) { } #endregion - if (goods.Is()) { - BuildBeltInserterInfo(gui, amount, recipe?.buildingCount ?? 0); + if (goods is { target: Item item }) { + BuildBeltInserterInfo(gui, item, amount, recipe?.buildingCount ?? 0); } } } @@ -1408,7 +1408,7 @@ private static List GetRecipesRecursive(RecipeRow recipeRoot) { private void BuildShoppingList(RecipeRow? recipeRoot) => ShoppingListScreen.Show(recipeRoot == null ? GetRecipesRecursive() : GetRecipesRecursive(recipeRoot)); - private static void BuildBeltInserterInfo(ImGui gui, float amount, float buildingCount) { + private static void BuildBeltInserterInfo(ImGui gui, Item item, float amount, float buildingCount) { var preferences = Project.current.preferences; var belt = preferences.defaultBelt; var inserter = preferences.defaultInserter; @@ -1431,7 +1431,7 @@ private static void BuildBeltInserterInfo(ImGui gui, float amount, float buildin } using (gui.EnterRow()) { - int capacity = preferences.inserterCapacity; + int capacity = Math.Min(item.stackSize, preferences.inserterCapacity); float inserterBase = inserter.inserterSwingTime * amount / capacity; click |= gui.BuildFactorioObjectButton(inserter, ButtonDisplayStyle.Default) == Click.Left; string text = DataUtils.FormatAmount(inserterBase, UnitOfMeasure.None); diff --git a/changelog.txt b/changelog.txt index 54e79f2d..5bd71a33 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ Date: Fixes: - (regression) Fix opening new unnamed files. - (.NET 9) Fix page name display in the search-all dropdown. + - When calculating the required inserters, remember that inserters cannot hold more than one stack. Internal changes: - Quality objects now have reference equality and abstract serialization, like FactorioObjects. ---------------------------------------------------------------------------------------------------------------------- From 50fd22e86c855d4271e0e133b8ef24b0366e8309 Mon Sep 17 00:00:00 2001 From: shpaass Date: Sat, 5 Apr 2025 10:33:57 +0200 Subject: [PATCH 11/54] release Yafc 2.11.1 --- Yafc/Yafc.csproj | 4 ++-- changelog.txt | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Yafc/Yafc.csproj b/Yafc/Yafc.csproj index 434208d4..9b9dd117 100644 --- a/Yafc/Yafc.csproj +++ b/Yafc/Yafc.csproj @@ -3,8 +3,8 @@ WinExe net8.0 win-x64;linux-x64;osx-x64;osx-arm64 - 2.11.0 - 2.11.0 + 2.11.1 + 2.11.1 true image.ico enable diff --git a/changelog.txt b/changelog.txt index 5bd71a33..be0548ce 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,9 @@ ---------------------------------------------------------------------------------------------------------------------- Version: Date: +---------------------------------------------------------------------------------------------------------------------- +Version: 2.11.1 +Date: April 5th 2025 Fixes: - (regression) Fix opening new unnamed files. - (.NET 9) Fix page name display in the search-all dropdown. From 57d4e9123ce82954438a793f8611318e8f173f3c Mon Sep 17 00:00:00 2001 From: Maarten Bezemer Date: Thu, 17 Apr 2025 16:03:24 +0200 Subject: [PATCH 12/54] chore(doc): Fix details of the Linux/OSX install guide (#458) --- Docs/LinuxOsxInstall.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Docs/LinuxOsxInstall.md b/Docs/LinuxOsxInstall.md index 21a82697..87f88243 100644 --- a/Docs/LinuxOsxInstall.md +++ b/Docs/LinuxOsxInstall.md @@ -1,20 +1,20 @@ ### Arch There is an AUR package for yafc-ce: [`factorio-yafc-ce-git`](https://aur.archlinux.org/packages/factorio-yafc-ce-git) -Once the package is installed, it can be run with `factorio-yafc`. Note that at least dotnet 6 or later is required. +Once the package is installed, it can be run with `factorio-yafc`. Note that dotnet runtime v8 is required. ### Debian-based - Download the latest Yafc-ce release. -- [Install dotnet core (v8.0 or later)](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian) +- [Install dotnet core (runtime version: v8)](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian) - Install SDL2: - `sudo apt-get install libsdl2-2.0-0` - `sudo apt-get install libsdl2-image-2.0-0` - `sudo apt-get install libsdl2-ttf-2.0-0` - For reference, have following libraries: SDL2-2.0.so.0, SDL2_ttf-2.0.so.0, SDL2_image-2.0.so.0 -- Make sure you have OpenGL available -- Use the `Yafc` executable to run. +- Make sure you have OpenGL available. +- Use the `Yafc` executable to run (you might need to give it executable permissions: `chmod +x Yafc`). ### OSX -- [Install dotnet core (v8.0 or later)](https://dotnet.microsoft.com/download) +- [Install dotnet core (runtime version: v8)](https://dotnet.microsoft.com/download) - For Arm64 Macs, that's it. You can skip to the final step of launching Yafc. - For Intel Macs, you can skip to the step of getting SDL libraries with `brew`. - If you want to build Lua from source, here's how you can do that: From d53128c4a8e70bfaea26f68654882b63c3be5ff3 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 19 Apr 2025 08:54:48 -0400 Subject: [PATCH 13/54] fix: Productivity usually cannot exceed +300% --- Yafc.Model/Data/DataClasses.cs | 1 + Yafc.Model/Model/RecipeParameters.cs | 6 ++++++ .../FactorioDataDeserializer_RecipeAndTechnology.cs | 1 + .../ProductionTable/ModuleCustomizationScreen.cs | 12 ++++++++++++ .../Workspace/ProductionTable/ProductionTableView.cs | 6 +++++- changelog.txt | 2 ++ 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index c6e616d3..fd141c3f 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -183,6 +183,7 @@ public class Recipe : RecipeOrTechnology { public Dictionary technologyProductivity { get; internal set; } = []; public bool preserveProducts { get; internal set; } public bool hidden { get; internal set; } + public float? maximumProductivity { get; internal set; } public bool HasIngredientVariants() { foreach (var ingredient in ingredients) { diff --git a/Yafc.Model/Model/RecipeParameters.cs b/Yafc.Model/Model/RecipeParameters.cs index 52aa1a75..62058df3 100644 --- a/Yafc.Model/Model/RecipeParameters.cs +++ b/Yafc.Model/Model/RecipeParameters.cs @@ -13,6 +13,7 @@ public enum WarningFlags { AsteroidCollectionNotModelled = 1 << 3, AssumesFulgoraAndModel = 1 << 4, UselessQuality = 1 << 5, + ExcessProductivity = 1 << 6, // Static errors EntityNotSpecified = 1 << 8, @@ -175,6 +176,11 @@ public static RecipeParameters CalculateParameters(IRecipeRow row) { activeEffects.speed += speed; activeEffects.consumption += consumption; + if (recipe.target is Recipe { maximumProductivity: float maxProd } && activeEffects.productivity > maxProd) { + warningFlags |= WarningFlags.ExcessProductivity; + activeEffects.productivity = maxProd; + } + recipeTime /= activeEffects.speedMod; fuelUsagePerSecondPerBuilding *= activeEffects.energyUsageMod; diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs index cfea2ae1..390cd6aa 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs @@ -372,5 +372,6 @@ private void LoadRecipeData(Recipe recipe, LuaTable table, ErrorCollector errorC recipe.hidden = table.Get("hidden", false); recipe.enabled = table.Get("enabled", true); + recipe.maximumProductivity = table.Get("maximum_productivity", 3f); } } diff --git a/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs b/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs index 85eda29d..69c7fa2c 100644 --- a/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs +++ b/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Yafc.Model; using Yafc.UI; @@ -128,6 +129,17 @@ void doToSelectedItem(EntityCrafter selectedCrafter) { effects.consumption += baseEffect.consumption; } + if (recipe?.recipe.target is Recipe actualRecipe) { + Dictionary levels = Project.current.settings.productivityTechnologyLevels; + foreach ((Technology productivityTechnology, float changePerLevel) in actualRecipe.technologyProductivity) { + if (levels.TryGetValue(productivityTechnology, out int productivityTechLevel)) { + effects.productivity += changePerLevel * productivityTechLevel; + } + } + + effects.productivity = Math.Min(effects.productivity, actualRecipe.maximumProductivity ?? float.MaxValue); + } + if (recipe != null) { float craftingSpeed = (recipe.entity?.GetCraftingSpeed() ?? 1f) * effects.speedMod; gui.BuildText("Current effects:", Font.subheader); diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index 43260908..39761205 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -6,7 +6,6 @@ using Yafc.Blueprints; using Yafc.Model; using Yafc.UI; -using static Yafc.UI.ImGui; namespace Yafc; @@ -82,6 +81,9 @@ public override void BuildElement(ImGui gui, RecipeRow row) { else if (row.warningFlags.HasFlag(WarningFlags.UselessQuality)) { _ = MainScreen.Instance.ShowPseudoScreen(new MilestonesPanel()); } + else if (row.warningFlags.HasFlag(WarningFlags.ExcessProductivity)) { + PreferencesScreen.ShowProgression(); + } } } else { @@ -1551,6 +1553,8 @@ protected override void BuildPageTooltip(ImGui gui, ProductionTable contents) { "The accumulator estimate tries to store 10% of the energy captured by the attractors."}, {WarningFlags.UselessQuality, "The quality bonus on this recipe has no effect. " + "Make sure the recipe produces items and that all milestones for the next quality are unlocked. (Click to open the milestone window)"}, + {WarningFlags.ExcessProductivity, "This building has a larger productivity bonus (from base effect, research, and/or modules) than allowed by the recipe. " + + "Please make sure you entered productivity research levels, not percent bonuses. (Click to open the preferences)"}, }; private static readonly (Icon icon, SchemeColor color)[] tagIcons = [ diff --git a/changelog.txt b/changelog.txt index be0548ce..6f553101 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,8 @@ ---------------------------------------------------------------------------------------------------------------------- Version: Date: + Fixes: + - Prevent productivity bonuses from exceeding +300%, unless otherwise allowed by mods. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 From aaa4bf4d2a288a65b4feaf29903aae7ad525a8cd Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 19 Apr 2025 20:32:07 -0400 Subject: [PATCH 14/54] fix(#416): Handle duplicate productivity effects correctly. --- .../Data/FactorioDataDeserializer_RecipeAndTechnology.cs | 7 ++++--- changelog.txt | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs index 390cd6aa..f052a07b 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs @@ -152,10 +152,11 @@ private void LoadTechnologyData(Technology technology, LuaTable table, ErrorColl continue; } - float change = modifier.Get("change", 0f); + _ = technology.changeRecipeProductivity.TryGetValue(recipe, out float change); + change += modifier.Get("change", 0f); - technology.changeRecipeProductivity.Add(recipe, change); - recipe.technologyProductivity.Add(technology, change); + technology.changeRecipeProductivity[recipe] = change; + recipe.technologyProductivity[technology] = change; break; } diff --git a/changelog.txt b/changelog.txt index 6f553101..42f2ed50 100644 --- a/changelog.txt +++ b/changelog.txt @@ -19,6 +19,7 @@ Version: Date: Fixes: - Prevent productivity bonuses from exceeding +300%, unless otherwise allowed by mods. + - When loading duplicate research productivity effects, obey both of them, instead of failing. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 From 634b3e8754d283baa77cd9f90999b1fd957ace5e Mon Sep 17 00:00:00 2001 From: Dorus Date: Mon, 21 Apr 2025 20:11:46 +0200 Subject: [PATCH 15/54] fix: Add check if item.weight and/or defaultItemWeight is zero. Write rocketCapacity to items so we never need to calculate it again in the UI. --- Yafc.Model/Data/DataClasses.cs | 1 + Yafc.Parser/Data/FactorioDataDeserializer.cs | 16 +++++++++------- Yafc/Widgets/ObjectTooltip.cs | 2 +- changelog.txt | 1 + 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index fd141c3f..867b210f 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -386,6 +386,7 @@ public Item() { public int stackSize { get; internal set; } public Entity? placeResult { get; internal set; } public Entity? plantResult { get; internal set; } + public int rocketCapacity { get; internal set; } public override bool isPower => false; public override string type => "Item"; internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Items; diff --git a/Yafc.Parser/Data/FactorioDataDeserializer.cs b/Yafc.Parser/Data/FactorioDataDeserializer.cs index b992cf58..2327b131 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer.cs @@ -559,11 +559,8 @@ private void CalculateItemWeights() { nextWeightCalculation:; } - List rocketSilos = [.. registeredObjects.Values.OfType().Where(e => e.factorioType == "rocket-silo")]; - int maxStacks = 1;// if we have no rocket silos, default to one stack. - if (rocketSilos.Count > 0) { - maxStacks = rocketSilos.Max(r => r.rocketInventorySize); - } + int maxStacks = registeredObjects.Values.OfType().Where(e => e.factorioType == "rocket-silo").MaxBy(e => e.rocketInventorySize)?.rocketInventorySize + ?? 1; // if we have no rocket silos, default to one stack. foreach (Item item in allObjects.OfType()) { // If it doesn't otherwise have a weight, it gets the default weight. @@ -571,11 +568,16 @@ private void CalculateItemWeights() { item.weight = defaultItemWeight; } + // The item count is initialized to 1, but it should be the rocket capacity. Scale up the ingredient and product(s). + // item.weight == 0 is possible if defaultItemWeight is 0, so we bail out on the / item.weight in that case. + int maxFactor = maxStacks * item.stackSize; + int factor = item.weight == 0 ? maxFactor : Math.Min(rocketCapacity / item.weight, maxFactor); + + item.rocketCapacity = factor; + if (registeredObjects.TryGetValue((typeof(Mechanics), SpecialNames.RocketLaunch + "." + item.name), out FactorioObject? r) && r is Mechanics recipe) { - // The item count is initialized to 1, but it should be the rocket capacity. Scale up the ingredient and product(s). - int factor = Math.Min(rocketCapacity / item.weight, maxStacks * item.stackSize); recipe.ingredients[0] = new(item, factor); for (int i = 0; i < recipe.products.Length; i++) { recipe.products[i] *= factor; diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index 2a878b01..0005b7db 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -465,7 +465,7 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { using (gui.EnterGroup(contentPadding)) { gui.BuildText("Stack size: " + item.stackSize); - gui.BuildText("Rocket capacity: " + DataUtils.FormatAmount(Database.rocketCapacity / item.weight, UnitOfMeasure.None)); + gui.BuildText("Rocket capacity: " + DataUtils.FormatAmount(item.rocketCapacity, UnitOfMeasure.None)); } } } diff --git a/changelog.txt b/changelog.txt index 42f2ed50..7803974f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ Date: Fixes: - Prevent productivity bonuses from exceeding +300%, unless otherwise allowed by mods. - When loading duplicate research productivity effects, obey both of them, instead of failing. + - Fixed a crash when item.weight == 0, related to ultracube. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 From 452c11decaf16144c06c81b77b4c855b05f3ede3 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:25:15 -0400 Subject: [PATCH 16/54] fix(#454): Ignore invalid version specifiers. --- Yafc.Parser/FactorioDataSource.cs | 4 +++- changelog.txt | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Yafc.Parser/FactorioDataSource.cs b/Yafc.Parser/FactorioDataSource.cs index 8876f872..f52b1de5 100644 --- a/Yafc.Parser/FactorioDataSource.cs +++ b/Yafc.Parser/FactorioDataSource.cs @@ -235,7 +235,9 @@ public static Project Parse(string factorioPath, string modPath, string projectP foreach (var mod in allFoundMods) { CurrentLoadingMod = mod.name; - if (mod.ValidForFactorioVersion(factorioVersion) && allMods.TryGetValue(mod.name, out var existing) && (existing == null || mod.parsedVersion > existing.parsedVersion || (mod.parsedVersion == existing.parsedVersion && existing.zipArchive != null && mod.zipArchive == null)) && (!versionSpecifiers.TryGetValue(mod.name, out var version) || mod.parsedVersion == version)) { + if (mod.ValidForFactorioVersion(factorioVersion) && allMods.TryGetValue(mod.name, out var existing) + && (existing == null || mod.parsedVersion > existing.parsedVersion || (mod.parsedVersion == existing.parsedVersion && existing.zipArchive != null && mod.zipArchive == null)) + && (!versionSpecifiers.TryGetValue(mod.name, out var version) || existing?.parsedVersion != version)) { existing?.Dispose(); allMods[mod.name] = mod; } diff --git a/changelog.txt b/changelog.txt index 7803974f..3f53fe98 100644 --- a/changelog.txt +++ b/changelog.txt @@ -21,6 +21,7 @@ Date: - Prevent productivity bonuses from exceeding +300%, unless otherwise allowed by mods. - When loading duplicate research productivity effects, obey both of them, instead of failing. - Fixed a crash when item.weight == 0, related to ultracube. + - If the requested mod version isn't found, use the latest, like Factorio. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 From ae39330cdfdce16a2031845081c721a5fcaf4e2f Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:57:39 -0400 Subject: [PATCH 17/54] change: Assign some conditions to named variables. --- Yafc.Parser/FactorioDataSource.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Yafc.Parser/FactorioDataSource.cs b/Yafc.Parser/FactorioDataSource.cs index f52b1de5..a27f6ee4 100644 --- a/Yafc.Parser/FactorioDataSource.cs +++ b/Yafc.Parser/FactorioDataSource.cs @@ -235,9 +235,12 @@ public static Project Parse(string factorioPath, string modPath, string projectP foreach (var mod in allFoundMods) { CurrentLoadingMod = mod.name; - if (mod.ValidForFactorioVersion(factorioVersion) && allMods.TryGetValue(mod.name, out var existing) - && (existing == null || mod.parsedVersion > existing.parsedVersion || (mod.parsedVersion == existing.parsedVersion && existing.zipArchive != null && mod.zipArchive == null)) - && (!versionSpecifiers.TryGetValue(mod.name, out var version) || existing?.parsedVersion != version)) { + ModInfo? existing = null; + bool modFound = mod.ValidForFactorioVersion(factorioVersion) && allMods.TryGetValue(mod.name, out existing); + bool higherVersionOrFolder = existing == null || mod.parsedVersion > existing.parsedVersion || (mod.parsedVersion == existing.parsedVersion && existing.zipArchive != null && mod.zipArchive == null); + bool existingMatchesVersionDirective = versionSpecifiers.TryGetValue(mod.name, out var version) && existing?.parsedVersion == version; + + if (modFound && higherVersionOrFolder && !existingMatchesVersionDirective) { existing?.Dispose(); allMods[mod.name] = mod; } From 5eb8958d5f848073b9a51b95c9bc7b1029e75b19 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 28 Apr 2025 05:23:35 -0400 Subject: [PATCH 18/54] fix(#459): Use correct amounts from other tabs in legacy summary. --- Yafc.Model/Model/ProductionSummary.cs | 5 ++--- changelog.txt | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Yafc.Model/Model/ProductionSummary.cs b/Yafc.Model/Model/ProductionSummary.cs index 9f7272da..9fa1b833 100644 --- a/Yafc.Model/Model/ProductionSummary.cs +++ b/Yafc.Model/Model/ProductionSummary.cs @@ -133,9 +133,8 @@ public void RefreshFlow() { } foreach (var link in subTable.allLinks) { - if (link.amount != 0) { - _ = flow.TryGetValue(link.goods, out float prevValue); - flow[link.goods] = prevValue + (link.amount * multiplier); + if (link.amount != 0 && !flow.ContainsKey(link.goods)) { + flow[link.goods] = link.amount * multiplier; } } } diff --git a/changelog.txt b/changelog.txt index 3f53fe98..b31f3d7d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -22,6 +22,7 @@ Date: - When loading duplicate research productivity effects, obey both of them, instead of failing. - Fixed a crash when item.weight == 0, related to ultracube. - If the requested mod version isn't found, use the latest, like Factorio. + - Fix amounts loaded from other pages in the legacy summary page. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 From acc6688a346bb639754f6d545bb7676f9536462d Mon Sep 17 00:00:00 2001 From: Dorus Date: Thu, 1 May 2025 17:44:58 +0200 Subject: [PATCH 19/54] Improve precision #445 --- Yafc.Model/Model/ProductionTableContent.cs | 2 +- changelog.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Yafc.Model/Model/ProductionTableContent.cs b/Yafc.Model/Model/ProductionTableContent.cs index 0ab21834..63bd45b1 100644 --- a/Yafc.Model/Model/ProductionTableContent.cs +++ b/Yafc.Model/Model/ProductionTableContent.cs @@ -626,7 +626,7 @@ public void ChangeVariant(T was, T now) where T : FactorioObject { [MemberNotNullWhen(true, nameof(subgroup))] public bool isOverviewMode => subgroup != null && !subgroup.expanded; - public float buildingCount => (float)recipesPerSecond * parameters.recipeTime; + public float buildingCount => (float)(recipesPerSecond * parameters.recipeTime); public bool visible { get; internal set; } = true; public RecipeRow(ProductionTable owner, IObjectWithQuality recipe) : base(owner) { diff --git a/changelog.txt b/changelog.txt index b31f3d7d..9ed75154 100644 --- a/changelog.txt +++ b/changelog.txt @@ -23,6 +23,7 @@ Date: - Fixed a crash when item.weight == 0, related to ultracube. - If the requested mod version isn't found, use the latest, like Factorio. - Fix amounts loaded from other pages in the legacy summary page. + - Improved precision for building count calculation. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 From 91654a727d50a1f8f87727fb712d7f555660d10c Mon Sep 17 00:00:00 2001 From: shpaass Date: Fri, 2 May 2025 16:08:37 +0200 Subject: [PATCH 20/54] Add Crowdin github action to trigger the update Crowdin is a platform to make translations easier. Dalestan brought my attention to it, so I'm adding it to see how it goes. --- .../workflows/crowdin-translation-action.yml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/crowdin-translation-action.yml diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml new file mode 100644 index 00000000..57a2eca0 --- /dev/null +++ b/.github/workflows/crowdin-translation-action.yml @@ -0,0 +1,34 @@ +name: Crowdin Action + +on: + push: + branches: [ main ] + +jobs: + synchronize-with-crowdin: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: crowdin action + uses: crowdin/github-action@v2 + with: + upload_sources: true + upload_translations: false + download_translations: true + localization_branch_name: l10n_crowdin_translations + create_pull_request: true + pull_request_title: 'New Crowdin Translations' + pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' + pull_request_base_branch_name: 'main' + env: + # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + + # A numeric ID, found at https://crowdin.com/project//tools/api + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + + # Visit https://crowdin.com/settings#api-key to create this token + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} From 8867511b588b1489be58a158bbd0320605447633 Mon Sep 17 00:00:00 2001 From: shpaass Date: Fri, 2 May 2025 16:28:10 +0200 Subject: [PATCH 21/54] ci: create a config file for the crowdin github Action As written in the manual, create crowdin.yml with the config info for the Crowdin github Action. Manual location: https://github.com/marketplace/actions/crowdin-action --- crowdin.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..8f901685 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,19 @@ +# This file has the info necessary for the setup of the Crowdin github Action. +# The info was taken from the page of the github Action: https://github.com/marketplace/actions/crowdin-action + +# By default, the Action will look for the crowdin.yml file in the root of the repository, so I put this file in there. +# Feel free to discuss moving it elsewhere if you can think of a better location. +# You can specify a different path using the "config" option in this file. + +"project_id_env": "CROWDIN_PROJECT_ID" +"api_token_env": "CROWDIN_PERSONAL_TOKEN" +"base_path": "." + +"preserve_hierarchy": true + +"files": [ + { + "source": "locales/en.yml", + "translation": "locales/%two_letters_code%.yml" + } +] From c72b36d57793ddcbd9b2fd69757a13d88e2ef03b Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 2 May 2025 08:04:24 -0400 Subject: [PATCH 22/54] fix(#465): Don't auto-calculate catalysts in Factorio 2.0. --- ...ctorioDataDeserializer_RecipeAndTechnology.cs | 16 +++++++++------- changelog.txt | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs index f052a07b..c3d996c9 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs @@ -56,13 +56,15 @@ private void DeserializeQuality(LuaTable table, ErrorCollector errorCollector) { } private void UpdateRecipeCatalysts() { - foreach (var recipe in allObjects.OfType()) { - foreach (var product in recipe.products) { - if (product.productivityAmount == product.amount) { - float catalyst = recipe.GetConsumptionPerRecipe(product.goods); - - if (catalyst > 0f) { - product.SetCatalyst(catalyst); + if (factorioVersion < new Version(2, 0, 0)) { + foreach (var recipe in allObjects.OfType()) { + foreach (var product in recipe.products) { + if (product.productivityAmount == product.amount) { + float catalyst = recipe.GetConsumptionPerRecipe(product.goods); + + if (catalyst > 0f) { + product.SetCatalyst(catalyst); + } } } } diff --git a/changelog.txt b/changelog.txt index 9ed75154..af35e351 100644 --- a/changelog.txt +++ b/changelog.txt @@ -24,6 +24,7 @@ Date: - If the requested mod version isn't found, use the latest, like Factorio. - Fix amounts loaded from other pages in the legacy summary page. - Improved precision for building count calculation. + - Remove automatic catalyst amount calculations when loading Factorio 2.0 data. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 From 3b78deac804fc1b051a59ced2ef4347c214b7701 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:57:03 -0400 Subject: [PATCH 23/54] change: Add missing languages to the selection list; use native names. --- Yafc/Windows/WelcomeScreen.cs | 69 +++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/Yafc/Windows/WelcomeScreen.cs b/Yafc/Windows/WelcomeScreen.cs index 7e89cecf..2fac740e 100644 --- a/Yafc/Windows/WelcomeScreen.cs +++ b/Yafc/Windows/WelcomeScreen.cs @@ -39,34 +39,57 @@ private string modsPath { private static readonly Dictionary languageMapping = new Dictionary() { {"en", "English"}, - {"ca", "Catalan"}, - {"cs", "Czech"}, - {"da", "Danish"}, - {"nl", "Dutch"}, - {"de", "German"}, - {"fi", "Finnish"}, - {"fr", "French"}, - {"hu", "Hungarian"}, - {"it", "Italian"}, - {"no", "Norwegian"}, - {"pl", "Polish"}, - {"pt-PT", "Portuguese"}, - {"pt-BR", "Portuguese (Brazilian)"}, - {"ro", "Romanian"}, - {"ru", "Russian"}, - {"es-ES", "Spanish"}, - {"sv-SE", "Swedish"}, - {"tr", "Turkish"}, - {"uk", "Ukrainian"}, + {"af", "Afrikaans" }, + {"be", "беларуская" }, + {"bg", "български" }, + {"ca", "català"}, + {"cs", "čeština"}, + {"da", "dansk"}, + {"de", "Deutsch"}, + {"el", "Ελληνικά" }, + {"es-ES", "español"}, + {"et", "eesti" }, + {"eu", "euskara" }, + {"fi", "suomi"}, + {"fil", "Filipino"}, + {"fr", "français"}, + {"fy-NL", "Frysk"}, + {"ga-IE", "Gaeilge"}, + {"hr", "hrvatski"}, + {"hu", "magyar"}, + {"id", "Bahasa Indonesia"}, + {"is", "íslenska"}, + {"it", "italiano"}, + {"kk", "Қазақша"}, + {"lt", "lietuvių"}, + {"lv", "latviešu"}, + {"nl", "Nederlands"}, + {"no", "norsk"}, + {"pl", "polski"}, + {"pt-PT", "português"}, + {"pt-BR", "português (Brazil)"}, + {"ro", "română"}, + {"ru", "русский"}, + {"sk", "slovenčina"}, + {"sl", "slovenski"}, + {"sq", "shqip"}, + {"sr", "српски"}, + {"sv-SE", "svenska"}, + {"tr", "türkmençe"}, + {"uk", "українська"}, + {"vi", "Tiếng Việt"}, }; private static readonly Dictionary languagesRequireFontOverride = new Dictionary() { + {"ar", "Arabic"}, + {"he", "Hebrew"}, {"ja", "Japanese"}, + {"ka", "Georgian"}, + {"ko", "Korean"}, + {"th", "Thai"}, {"zh-CN", "Chinese (Simplified)"}, {"zh-TW", "Chinese (Traditional)"}, - {"ko", "Korean"}, - {"tr", "Turkish"}, }; private enum EditType { @@ -242,9 +265,9 @@ private void ProjectErrorMoreInfo(ImGui gui) { private static void DoLanguageList(ImGui gui, Dictionary list, bool enabled) { foreach (var (k, v) in list) { if (!enabled) { - gui.BuildText(v); + gui.BuildText(v + " (" + k + ")"); } - else if (gui.BuildLink(v)) { + else if (gui.BuildLink(v + " (" + k + ")")) { Preferences.Instance.language = k; Preferences.Instance.Save(); _ = gui.CloseDropdown(); From 7630c073cd98a920905164d4b638a911956291ca Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 19 Apr 2025 15:39:49 -0400 Subject: [PATCH 24/54] feat: Optionally, download missing fonts when changing the language. Update language documentation. --- Docs/MoreLanguagesSupport.md | 24 ++-- Yafc.UI/Rendering/Font.cs | 18 +++ Yafc/Program.cs | 11 +- Yafc/Windows/AboutScreen.cs | 1 + Yafc/Windows/WelcomeScreen.cs | 216 ++++++++++++++++++++++++---------- changelog.txt | 4 + licenses.txt | 97 +++++++++++++++ 7 files changed, 294 insertions(+), 77 deletions(-) diff --git a/Docs/MoreLanguagesSupport.md b/Docs/MoreLanguagesSupport.md index 9e19b7d5..7f3fa704 100644 --- a/Docs/MoreLanguagesSupport.md +++ b/Docs/MoreLanguagesSupport.md @@ -1,17 +1,21 @@ # YAFC support for more languages -YAFC language support is experimental. If your language is missing, that is probably because of one of two reasons: +You can ask Yafc to display non-English names for Factorio objects from the Welcome screen: +- On the Welcome screen, click the language name (probably "English") next to "In-game objects language:" +- Select your language from the drop-down that appears. +- If your language uses non-European glyphs, it may appear at the bottom of the list. + - To use these languages, Yafc may need to do a one-time download of a suitable font. +Click "Confirm" if Yafc asks permission to download a font. + - If you do not wish to have Yafc automatically download a suitable font, click "Select font" in the drop-down, and select a font file that supports your language. -- It has less than 90% support in official Factorio translation -- It uses non-European glyphs (such as Chinese or Japanese languages) - -You can enable support for your language using this method: -- Navigate to `yafc.config` file located at `%localappdata%\YAFC` (`C:\Users\username\AppData\Local\YAFC`). Open it with the text editor. -- Find `language` section and replace the value with your language code. Here are examples of language codes: - - Chinese (Simplified): `zh-CN` +If your language is supported by Factorio but does not appear in the Welcome screen, you can manually force YAFC to use the strings for your language: +- Navigate to `yafc2.config` file located at `%localappdata%\YAFC` (`C:\Users\username\AppData\Local\YAFC`). Open it with a text editor. +- Find the `language` section and replace the value with your language code. Here are examples of language codes: + - Chinese (Simplified): `zh-CN` - Chinese (Traditional): `zh-TW` - Korean: `ko` - Japanese: `ja` - Hebrew: `he` - - Else: Look into `Factorio/data/base/locale` folder and find folder with your language. -- If your language have non-European glyphs, you also need to replace fonts: `Yafc/Data/Roboto-Light.ttf` and `Roboto-Regular.ttf` with any fonts that support your language glyphs. \ No newline at end of file + - Else: Look into `Factorio/data/base/locale` folder and find the folder with your language. +- If your language uses non-European glyphs, you also need to replace the fonts `Yafc/Data/Roboto-Light.ttf` and `Roboto-Regular.ttf` with fonts that support your language. +You may also use the "Select font" button in the language dropdown on the Welcome screen to change the font. diff --git a/Yafc.UI/Rendering/Font.cs b/Yafc.UI/Rendering/Font.cs index c7a1f9b5..5fcee3f8 100644 --- a/Yafc.UI/Rendering/Font.cs +++ b/Yafc.UI/Rendering/Font.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using SDL2; namespace Yafc.UI; @@ -23,11 +24,28 @@ public FontFile.FontSize GetFontSize(float pixelsPreUnit) { return lastFontSize; } + /// + /// Returns if the current font has glyphs for all characters in the supplied string, or if + /// any characters do not have a glyph. + /// + public bool CanDraw(string value) { + nint handle = lastFontSize!.handle; + foreach (char ch in value) { + if (SDL_ttf.TTF_GlyphIsProvided(handle, ch) == 0) { + return false; + } + } + return true; + } + public IntPtr GetHandle(float pixelsPreUnit) => GetFontSize(pixelsPreUnit).handle; public float GetLineSize(float pixelsPreUnit) => GetFontSize(pixelsPreUnit).lineSize / pixelsPreUnit; public void Dispose() => file.Dispose(); + + public static bool FilesExist(string baseFileName) + => File.Exists($"Data/{baseFileName}-Regular.ttf") && File.Exists($"Data/{baseFileName}-Light.ttf"); } public sealed class FontFile(string fileName) : IDisposable { diff --git a/Yafc/Program.cs b/Yafc/Program.cs index 109ff82c..c3a4950f 100644 --- a/Yafc/Program.cs +++ b/Yafc/Program.cs @@ -23,9 +23,16 @@ private static void Main(string[] args) { Console.Error.WriteException(ex); } + string baseFileName = "Roboto"; + if (WelcomeScreen.languageMapping.TryGetValue(Preferences.Instance.language, out LanguageInfo? language)) { + if (Font.FilesExist(language.BaseFontName)) { + baseFileName = language.BaseFontName; + } + } + hasOverriddenFont = overriddenFontFile != null; - Font.header = new Font(overriddenFontFile ?? new FontFile("Data/Roboto-Light.ttf"), 2f); - var regular = overriddenFontFile ?? new FontFile("Data/Roboto-Regular.ttf"); + Font.header = new Font(overriddenFontFile ?? new FontFile($"Data/{baseFileName}-Light.ttf"), 2f); + var regular = overriddenFontFile ?? new FontFile($"Data/{baseFileName}-Regular.ttf"); Font.subheader = new Font(regular, 1.5f); Font.productionTableHeader = new Font(regular, 1.23f); Font.text = new Font(regular, 1f); diff --git a/Yafc/Windows/AboutScreen.cs b/Yafc/Windows/AboutScreen.cs index 3cd223e1..73f89fa4 100644 --- a/Yafc/Windows/AboutScreen.cs +++ b/Yafc/Windows/AboutScreen.cs @@ -57,6 +57,7 @@ protected override void BuildContents(ImGui gui) { gui.BuildText("Google"); BuildLink(gui, "https://developers.google.com/optimization", "OR-Tools,"); BuildLink(gui, "https://fonts.google.com/specimen/Roboto", "Roboto font family"); + BuildLink(gui, "https://fonts.google.com/noto", "Noto Sans font family"); gui.BuildText("and"); BuildLink(gui, "https://material.io/resources/icons", "Material Design Icon collection"); } diff --git a/Yafc/Windows/WelcomeScreen.cs b/Yafc/Windows/WelcomeScreen.cs index 2fac740e..e5498d0b 100644 --- a/Yafc/Windows/WelcomeScreen.cs +++ b/Yafc/Windows/WelcomeScreen.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Net.Http; +using System.Reflection; using System.Runtime.InteropServices; +using Newtonsoft.Json.Linq; using SDL2; using Serilog; using Yafc.Model; @@ -12,9 +17,29 @@ namespace Yafc; +/// +/// Contains information about a language supported by Factorio. +/// +/// The name of the language, expressed in its language. To select this language, the current font must contain glyphs +/// for all characters in this string. +/// , if that uses only well-supported Latin characters, or the English name of the +/// language. +/// The base name of the default font to use for this language. +internal sealed record LanguageInfo(string LatinName, string NativeName, string BaseFontName) { + public LanguageInfo(string latinName, string nativeName) : this(latinName, nativeName, "Roboto") { } + public LanguageInfo(string name) : this(name, name, "Roboto") { } + + /// + /// If not , the current font must also have glyphs for these characters. Some fonts have enough glyphs for the + /// language name, but don't have glyphs for all characters used by the language. (e.g. Some fonts have glyphs for türkmençe but are missing + /// the Turkish ı.) + /// + public string? CheckExtraCharacters { get; set; } +} + public class WelcomeScreen : WindowUtility, IProgress<(string, string)>, IKeyboardFocus { private readonly ILogger logger = Logging.GetLogger(); - private bool loading; + private bool loading, downloading; private string? currentLoad1, currentLoad2; private string path = "", dataPath = "", _modsPath = ""; private string modsPath { @@ -36,60 +61,56 @@ private string modsPath { private readonly string[] tips; private bool useMostRecentSave = true; - private static readonly Dictionary languageMapping = new Dictionary() + internal static readonly SortedList languageMapping = new() { - {"en", "English"}, - {"af", "Afrikaans" }, - {"be", "беларуская" }, - {"bg", "български" }, - {"ca", "català"}, - {"cs", "čeština"}, - {"da", "dansk"}, - {"de", "Deutsch"}, - {"el", "Ελληνικά" }, - {"es-ES", "español"}, - {"et", "eesti" }, - {"eu", "euskara" }, - {"fi", "suomi"}, - {"fil", "Filipino"}, - {"fr", "français"}, - {"fy-NL", "Frysk"}, - {"ga-IE", "Gaeilge"}, - {"hr", "hrvatski"}, - {"hu", "magyar"}, - {"id", "Bahasa Indonesia"}, - {"is", "íslenska"}, - {"it", "italiano"}, - {"kk", "Қазақша"}, - {"lt", "lietuvių"}, - {"lv", "latviešu"}, - {"nl", "Nederlands"}, - {"no", "norsk"}, - {"pl", "polski"}, - {"pt-PT", "português"}, - {"pt-BR", "português (Brazil)"}, - {"ro", "română"}, - {"ru", "русский"}, - {"sk", "slovenčina"}, - {"sl", "slovenski"}, - {"sq", "shqip"}, - {"sr", "српски"}, - {"sv-SE", "svenska"}, - {"tr", "türkmençe"}, - {"uk", "українська"}, - {"vi", "Tiếng Việt"}, - }; - - private static readonly Dictionary languagesRequireFontOverride = new Dictionary() - { - {"ar", "Arabic"}, - {"he", "Hebrew"}, - {"ja", "Japanese"}, - {"ka", "Georgian"}, - {"ko", "Korean"}, - {"th", "Thai"}, - {"zh-CN", "Chinese (Simplified)"}, - {"zh-TW", "Chinese (Traditional)"}, + {"en", new("English")}, + {"af", new("Afrikaans")}, + {"ar", new("Arabic", "العربية", "noto-sans-arabic")}, + {"be", new("Belarusian", "беларуская")}, + {"bg", new("Bulgarian", "български")}, + {"ca", new("català")}, + {"cs", new("Czech", "čeština")}, + {"da", new("dansk")}, + {"de", new("Deutsch")}, + {"el", new("Greek", "Ελληνικά")}, + {"es-ES", new("español")}, + {"et", new("eesti") }, + {"eu", new("euskara") }, + {"fi", new("suomi")}, + {"fil", new("Filipino")}, + {"fr", new("français")}, + {"fy-NL", new("Frysk")}, + {"ga-IE", new("Gaeilge")}, + {"he", new("Hebrew", "עברית", "noto-sans-hebrew")}, + {"hr", new("hrvatski")}, + {"hu", new("magyar")}, + {"id", new("Bahasa Indonesia")}, + {"is", new("íslenska")}, + {"it", new("italiano")}, + {"ja", new("Japanese", "日本語", "noto-sans-jp") { CheckExtraCharacters = "軽気処鉱砕" }}, + {"ka", new("Georgian", "ქართული", "noto-sans-georgian")}, + {"kk", new("Kazakh", "Қазақша")}, + {"ko", new("Korean", "한국어", "noto-sans-kr")}, + {"lt", new("Lithuanian", "lietuvių")}, + {"lv", new("Latvian", "latviešu")}, + {"nl", new("Nederlands")}, + {"no", new("norsk")}, + {"pl", new("polski")}, + {"pt-PT", new("português")}, + {"pt-BR", new("português (Brazil)")}, + {"ro", new("română")}, + {"ru", new("Russian", "русский")}, + {"sk", new("Slovak", "slovenčina")}, + {"sl", new("slovenski")}, + {"sq", new("shqip")}, + {"sr", new("Serbian", "српски")}, + {"sv-SE", new("svenska")}, + {"th", new("Thai", "ไทย", "noto-sans-thai")}, + {"tr", new("Turkish","türkmençe") { CheckExtraCharacters = "ığş" }}, + {"uk", new("Ukranian", "українська")}, + {"vi", new("Vietnamese", "Tiếng Việt")}, + {"zh-CN", new("Chinese (Simplified)", "汉语", "noto-sans-sc")}, + {"zh-TW", new("Chinese (Traditional)", "漢語", "noto-sans-tc") { CheckExtraCharacters = "鈾礦" }}, }; private enum EditType { @@ -138,6 +159,10 @@ protected override void BuildContents(ImGui gui) { gui.BuildText(tip, new TextBlockDisplayStyle(WrapText: true, Alignment: RectAlignment.Middle)); gui.SetNextRebuild(Ui.time + 30); } + else if (downloading) { + gui.BuildText("Please wait . . .", TextBlockDisplayStyle.Centered); + gui.BuildText("Yafc is downloading the fonts for your language.", TextBlockDisplayStyle.Centered); + } else if (errorMessage != null) { errorScroll.Build(gui); bool thereIsAModToDisable = (errorMod != null); @@ -183,8 +208,8 @@ protected override void BuildContents(ImGui gui) { using (gui.EnterRow()) { gui.allocator = RectAllocator.RightRow; string lang = Preferences.Instance.language; - if (languageMapping.TryGetValue(Preferences.Instance.language, out string? mapped) || languagesRequireFontOverride.TryGetValue(Preferences.Instance.language, out mapped)) { - lang = mapped; + if (languageMapping.TryGetValue(Preferences.Instance.language, out LanguageInfo? mapped)) { + lang = mapped.NativeName; } if (gui.BuildLink(lang)) { @@ -262,17 +287,78 @@ private void ProjectErrorMoreInfo(ImGui gui) { gui.BuildWrappedText("Please attach a new-game save file to sync mods, versions, and settings."); } - private static void DoLanguageList(ImGui gui, Dictionary list, bool enabled) { + private void DoLanguageList(ImGui gui, SortedList list, bool listFontSupported) { foreach (var (k, v) in list) { - if (!enabled) { - gui.BuildText(v + " (" + k + ")"); + if (!Font.text.CanDraw(v.NativeName + v.CheckExtraCharacters) && !listFontSupported) { + bool result; + if (Font.text.CanDraw(v.NativeName)) { + result = gui.BuildLink($"{v.NativeName} ({k})"); + } + else { + result = gui.BuildLink($"{v.LatinName} ({k})"); + } + + if (result) { + if (Font.FilesExist(v.BaseFontName) || Program.hasOverriddenFont) { + Preferences.Instance.language = k; + Preferences.Instance.Save(); + gui.CloseDropdown(); + restartIfNecessary(); + } + else { + gui.ShowDropDown(async gui => { + gui.BuildText("Yafc will download a suitable font before it restarts.\nThis may take a minute or two.", TextBlockDisplayStyle.WrappedText); + gui.allocator = RectAllocator.Center; + if (gui.BuildButton("Confirm")) { + gui.CloseDropdown(); + downloading = true; + // Jump through several hoops to download an appropriate Noto Sans font. + // Google Webfonts Helper is the first place I found that would (eventually) let me directly download ttf files + // for the static fonts. It can also provide links to Google, but that would take three requests, instead of two. + // Variable fonts can be acquired from github, but SDL_ttf doesn't support those yet. + // See https://gwfh.mranftl.com/fonts, https://github.com/majodev/google-webfonts-helper, and + // https://github.com/libsdl-org/SDL_ttf/pull/506 + HttpClient client = new(); + // Get the character subsets supported by this font + string query = "https://gwfh.mranftl.com/api/fonts/" + v.BaseFontName; + dynamic response = JObject.Parse(await client.GetStringAsync(query)); + // Request a zip containing ttf files and all character subsets + query += "?variants=300,regular&download=zip&formats=ttf&subsets="; + foreach (string item in response["subsets"]) { + query += item + ","; + } + ZipArchive archive = new(await client.GetStreamAsync(query)); + // Extract the two ttf files into the expected locations + foreach (var entry in archive.Entries) { + if (entry.Name.Contains("300")) { + entry.ExtractToFile($"Data/{v.BaseFontName}-Light.ttf"); + } + else { + entry.ExtractToFile($"Data/{v.BaseFontName}-Regular.ttf"); + } + } + // Save and restart + Preferences.Instance.language = k; + Preferences.Instance.Save(); + restartIfNecessary(); + } + }); + } + } } - else if (gui.BuildLink(v + " (" + k + ")")) { + else if (Font.text.CanDraw(v.NativeName + v.CheckExtraCharacters) && listFontSupported && gui.BuildLink(v.NativeName + " (" + k + ")")) { Preferences.Instance.language = k; Preferences.Instance.Save(); _ = gui.CloseDropdown(); } } + + static void restartIfNecessary() { + if (!Program.hasOverriddenFont) { + Process.Start("dotnet", Assembly.GetEntryAssembly()!.Location); + Environment.Exit(0); + } + } } private void LanguageSelection(ImGui gui) { @@ -285,14 +371,14 @@ private void LanguageSelection(ImGui gui) { if (!Program.hasOverriddenFont) { gui.AllocateSpacing(0.5f); - string nonEuLanguageMessage = "To select languages with non-European glyphs you need to override used font first. Download or locate a font that has your language glyphs."; - gui.BuildText(nonEuLanguageMessage, TextBlockDisplayStyle.WrappedText); + string unsupportedLanguageMessage = "These languages are not supported by the current font. Click the language to restart with a suitable font, or click 'Select font' to select a custom font."; + gui.BuildText(unsupportedLanguageMessage, TextBlockDisplayStyle.WrappedText); gui.AllocateSpacing(0.5f); } - DoLanguageList(gui, languagesRequireFontOverride, Program.hasOverriddenFont); + DoLanguageList(gui, languageMapping, false); gui.AllocateSpacing(0.5f); - if (gui.BuildButton("Select font to override")) { + if (gui.BuildButton("Select font")) { SelectFont(); } @@ -304,7 +390,7 @@ private void LanguageSelection(ImGui gui) { Preferences.Instance.Save(); } } - gui.BuildText("Selecting font to override require YAFC restart to take effect", TextBlockDisplayStyle.WrappedText); + gui.BuildText("Restart Yafc to switch to the selected font.", TextBlockDisplayStyle.WrappedText); } private async void SelectFont() { diff --git a/changelog.txt b/changelog.txt index af35e351..623f360c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,9 @@ ---------------------------------------------------------------------------------------------------------------------- Version: Date: + Features: + - When changing the language for Factorio objects, automatically switch to, or download, a font that can + display that language. Fixes: - Prevent productivity bonuses from exceeding +300%, unless otherwise allowed by mods. - When loading duplicate research productivity effects, obey both of them, instead of failing. @@ -25,6 +28,7 @@ Date: - Fix amounts loaded from other pages in the legacy summary page. - Improved precision for building count calculation. - Remove automatic catalyst amount calculations when loading Factorio 2.0 data. + - Update documentation for changing the selected Factorio-object language. ---------------------------------------------------------------------------------------------------------------------- Version: 2.11.1 Date: April 5th 2025 diff --git a/licenses.txt b/licenses.txt index 1056f361..207abefd 100644 --- a/licenses.txt +++ b/licenses.txt @@ -908,6 +908,103 @@ https://github.com/googlefonts/roboto limitations under the License. +### Noto Sans font family ### +https://github.com/notofonts + + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + + ### Google material design icons ### https://github.com/google/material-design-icons From 8227245a89f6afe6fa195b4c729cc216a712393e Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Thu, 24 Apr 2025 01:14:28 -0400 Subject: [PATCH 25/54] change: Adjust FormatAmount calls for better i18n/l10n. --- Yafc.Model/Analysis/CostAnalysis.cs | 2 +- Yafc.Model/Data/DataUtils.cs | 15 ++++----------- Yafc.Parser/FactorioDataSource.cs | 10 ++++++++++ Yafc/Utils/Preferences.cs | 15 ++++++++++++++- Yafc/Widgets/ImmediateWidgets.cs | 3 ++- Yafc/Widgets/ObjectTooltip.cs | 18 +++++++++--------- Yafc/Windows/NeverEnoughItemsPanel.cs | 2 +- Yafc/Windows/ShoppingListScreen.cs | 4 ++-- .../ProductionTable/ProductionTableView.cs | 2 +- 9 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Yafc.Model/Analysis/CostAnalysis.cs b/Yafc.Model/Analysis/CostAnalysis.cs index e7680362..d207f85f 100644 --- a/Yafc.Model/Analysis/CostAnalysis.cs +++ b/Yafc.Model/Analysis/CostAnalysis.cs @@ -415,6 +415,6 @@ public static string GetDisplayCost(FactorioObject goods) { return null; } - return DataUtils.FormatAmount(itemFlow * 1000f, UnitOfMeasure.None, itemAmountPrefix); + return itemAmountPrefix + DataUtils.FormatAmount(itemFlow * 1000f, UnitOfMeasure.None); } } diff --git a/Yafc.Model/Data/DataUtils.cs b/Yafc.Model/Data/DataUtils.cs index 1980be06..bc190324 100644 --- a/Yafc.Model/Data/DataUtils.cs +++ b/Yafc.Model/Data/DataUtils.cs @@ -564,25 +564,22 @@ public static string FormatTime(float time) { return $"{time / 3600f:#} hours"; } - public static string FormatAmount(float amount, UnitOfMeasure unit, string? prefix = null, string? suffix = null, bool precise = false) { + public static string FormatAmount(float amount, UnitOfMeasure unit, bool precise = false) { var (multiplier, unitSuffix) = Project.current == null ? (1f, null) : Project.current.ResolveUnitOfMeasure(unit); - return FormatAmountRaw(amount, multiplier, unitSuffix, precise ? PreciseFormat : FormatSpec, prefix, suffix); + return FormatAmountRaw(amount, multiplier, unitSuffix, precise ? PreciseFormat : FormatSpec); } - public static string FormatAmountRaw(float amount, float unitMultiplier, string? unitSuffix, (char suffix, float multiplier, string format)[] formatSpec, string? prefix = null, string? suffix = null) { + public static string FormatAmountRaw(float amount, float unitMultiplier, string? unitSuffix, (char suffix, float multiplier, string format)[] formatSpec) { if (float.IsNaN(amount) || float.IsInfinity(amount)) { return "-"; } if (amount == 0f) { - return prefix + "0" + unitSuffix + suffix; + return "0" + unitSuffix; } _ = amountBuilder.Clear(); - if (prefix != null) { - _ = amountBuilder.Append(prefix); - } if (amount < 0) { _ = amountBuilder.Append('-'); @@ -600,10 +597,6 @@ public static string FormatAmountRaw(float amount, float unitMultiplier, string? _ = amountBuilder.Append(unitSuffix); - if (suffix != null) { - _ = amountBuilder.Append(suffix); - } - return amountBuilder.ToString(); } diff --git a/Yafc.Parser/FactorioDataSource.cs b/Yafc.Parser/FactorioDataSource.cs index a27f6ee4..8d716b4a 100644 --- a/Yafc.Parser/FactorioDataSource.cs +++ b/Yafc.Parser/FactorioDataSource.cs @@ -121,6 +121,16 @@ private static void LoadModLocale(string modName, string locale) { } } + public static void LoadYafcLocale(string locale) { + try { + foreach (string localeName in Directory.EnumerateFiles("Data/locale/" + locale + "/")) { + using Stream stream = File.Open(localeName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + FactorioLocalization.Parse(stream); + } + } + catch (DirectoryNotFoundException) { /* No Yafc translation for this locale */ } + } + private static void FindMods(string directory, IProgress<(string, string)> progress, List mods) { foreach (string entry in Directory.EnumerateDirectories(directory)) { string infoFile = Path.Combine(entry, "info.json"); diff --git a/Yafc/Utils/Preferences.cs b/Yafc/Utils/Preferences.cs index 4ff511bb..3bccf718 100644 --- a/Yafc/Utils/Preferences.cs +++ b/Yafc/Utils/Preferences.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Text.Json; using Yafc.Model; +using Yafc.Parser; namespace Yafc; @@ -11,6 +12,7 @@ public class Preferences { public static readonly Preferences Instance; public static readonly string appDataFolder; private static readonly string fileName; + private string _language = "en"; static Preferences() { appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); @@ -41,7 +43,18 @@ public void Save() { } public ProjectDefinition[] recentProjects { get; set; } = []; public bool darkMode { get; set; } - public string language { get; set; } = "en"; + public string language { + get => _language; + set { + _language = value; + // An intentional but possibly undesirable choice: We never reload the English locale unless the user explicitly selects English. + // As a result, a user could select pt-BR, then pt-PT, and use Yafc strings first from Portuguese, then Brazilian Portuguese, and + // then from English. + // This configuration cannot be saved and does not propagate to the mods. + // TODO: Update i18n to support proper fallbacking, both in Yafc and Factorio strings. + FactorioDataSource.LoadYafcLocale(value); + } + } public string? overrideFont { get; set; } /// /// Whether or not the main screen should be created maximized. diff --git a/Yafc/Widgets/ImmediateWidgets.cs b/Yafc/Widgets/ImmediateWidgets.cs index 15801e25..572bb0ca 100644 --- a/Yafc/Widgets/ImmediateWidgets.cs +++ b/Yafc/Widgets/ImmediateWidgets.cs @@ -299,7 +299,8 @@ public static void ShowPrecisionValueTooltip(ImGui gui, DisplayAmount amount, IF text = perSecond + "\n" + perMinute + "\n" + perHour; if (goods.target is Item item) { - text += DataUtils.FormatAmount(MathF.Abs(item.stackSize / amount.Value), UnitOfMeasure.Second, "\n", " per stack"); + text += "\n"; + text += DataUtils.FormatAmount(MathF.Abs(item.stackSize / amount.Value), UnitOfMeasure.Second) + " per stack"; } break; diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index 0005b7db..9397be84 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -239,18 +239,18 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, crafter.recipes, 2); if (crafter.CraftingSpeed(quality) != 1f) { - gui.BuildText(DataUtils.FormatAmount(crafter.CraftingSpeed(quality), UnitOfMeasure.Percent, "Crafting speed: ")); + gui.BuildText("Crafting speed: " + DataUtils.FormatAmount(crafter.CraftingSpeed(quality), UnitOfMeasure.Percent)); } Effect baseEffect = crafter.effectReceiver.baseEffect; if (baseEffect.speed != 0f) { - gui.BuildText(DataUtils.FormatAmount(baseEffect.speed, UnitOfMeasure.Percent, "Crafting speed: ")); + gui.BuildText("Crafting speed: " + DataUtils.FormatAmount(baseEffect.speed, UnitOfMeasure.Percent)); } if (baseEffect.productivity != 0f) { - gui.BuildText(DataUtils.FormatAmount(baseEffect.productivity, UnitOfMeasure.Percent, "Crafting productivity: ")); + gui.BuildText("Crafting productivity: " + DataUtils.FormatAmount(baseEffect.productivity, UnitOfMeasure.Percent)); } if (baseEffect.consumption != 0f) { - gui.BuildText(DataUtils.FormatAmount(baseEffect.consumption, UnitOfMeasure.Percent, "Energy consumption: ")); + gui.BuildText("Energy consumption: " + DataUtils.FormatAmount(baseEffect.consumption, UnitOfMeasure.Percent)); } if (crafter.allowedEffects != AllowedEffects.None) { @@ -442,23 +442,23 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { BuildSubHeader(gui, "Module parameters"); using (gui.EnterGroup(contentPadding)) { if (moduleSpecification.baseProductivity != 0f) { - gui.BuildText(DataUtils.FormatAmount(moduleSpecification.Productivity(quality), UnitOfMeasure.Percent, "Productivity: ")); + gui.BuildText("Productivity: " + DataUtils.FormatAmount(moduleSpecification.Productivity(quality), UnitOfMeasure.Percent)); } if (moduleSpecification.baseSpeed != 0f) { - gui.BuildText(DataUtils.FormatAmount(moduleSpecification.Speed(quality), UnitOfMeasure.Percent, "Speed: ")); + gui.BuildText("Speed: " + DataUtils.FormatAmount(moduleSpecification.Speed(quality), UnitOfMeasure.Percent)); } if (moduleSpecification.baseConsumption != 0f) { - gui.BuildText(DataUtils.FormatAmount(moduleSpecification.Consumption(quality), UnitOfMeasure.Percent, "Consumption: ")); + gui.BuildText("Consumption: " + DataUtils.FormatAmount(moduleSpecification.Consumption(quality), UnitOfMeasure.Percent)); } if (moduleSpecification.basePollution != 0f) { - gui.BuildText(DataUtils.FormatAmount(moduleSpecification.Pollution(quality), UnitOfMeasure.Percent, "Pollution: ")); + gui.BuildText("Pollution: " + DataUtils.FormatAmount(moduleSpecification.Pollution(quality), UnitOfMeasure.Percent)); } if (moduleSpecification.baseQuality != 0f) { - gui.BuildText(DataUtils.FormatAmount(moduleSpecification.Quality(quality), UnitOfMeasure.Percent, "Quality: ")); + gui.BuildText("Quality: " + DataUtils.FormatAmount(moduleSpecification.Quality(quality), UnitOfMeasure.Percent)); } } } diff --git a/Yafc/Windows/NeverEnoughItemsPanel.cs b/Yafc/Windows/NeverEnoughItemsPanel.cs index e758601b..2889b48f 100644 --- a/Yafc/Windows/NeverEnoughItemsPanel.cs +++ b/Yafc/Windows/NeverEnoughItemsPanel.cs @@ -186,7 +186,7 @@ private void DrawRecipeEntry(ImGui gui, RecipeEntry entry, bool production) { } float bh = CostAnalysis.GetBuildingHours(recipe, entry.recipeFlow); if (bh > 20) { - gui.BuildText(DataUtils.FormatAmount(bh, UnitOfMeasure.None, suffix: "bh"), TextBlockDisplayStyle.Centered); + gui.BuildText(DataUtils.FormatAmount(bh, UnitOfMeasure.None) + "bh", TextBlockDisplayStyle.Centered); _ = gui.BuildButton(gui.lastRect, SchemeColor.None, SchemeColor.Grey) .WithTooltip(gui, "Building-hours.\nAmount of building-hours required for all researches assuming crafting speed of 1"); diff --git a/Yafc/Windows/ShoppingListScreen.cs b/Yafc/Windows/ShoppingListScreen.cs index 0665ac0d..dc0e86f5 100644 --- a/Yafc/Windows/ShoppingListScreen.cs +++ b/Yafc/Windows/ShoppingListScreen.cs @@ -44,7 +44,7 @@ private ShoppingListScreen(List recipes) : base(42) { private void ElementDrawer(ImGui gui, (IObjectWithQuality obj, float count) element, int index) { using (gui.EnterRow()) { gui.BuildFactorioObjectIcon(element.obj, new IconDisplayStyle(2, MilestoneDisplay.Contained, false)); - gui.RemainingRow().BuildText(DataUtils.FormatAmount(element.count, UnitOfMeasure.None, "x") + ": " + element.obj.target.locName); + gui.RemainingRow().BuildText("x" + DataUtils.FormatAmount(element.count, UnitOfMeasure.None) + ": " + element.obj.target.locName); } _ = gui.BuildFactorioObjectButtonBackground(gui.lastRect, element.obj); } @@ -120,7 +120,7 @@ private static readonly (string, string?)[] assumeAdequateOptions = [ public override void Build(ImGui gui) { BuildHeader(gui, "Shopping list"); gui.BuildText( - "Total cost of all objects: " + DataUtils.FormatAmount(shoppingCost, UnitOfMeasure.None, "¥") + ", buildings: " + + "Total cost of all objects: ¥" + DataUtils.FormatAmount(shoppingCost, UnitOfMeasure.None) + ", buildings: " + DataUtils.FormatAmount(totalBuildings, UnitOfMeasure.None) + ", modules: " + DataUtils.FormatAmount(totalModules, UnitOfMeasure.None), TextBlockDisplayStyle.Centered); using (gui.EnterRow()) { if (gui.BuildRadioGroup(displayStateOptions, (int)displayState, out int newSelected)) { diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index 39761205..ed8aadde 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -1450,7 +1450,7 @@ private static void BuildBeltInserterInfo(ImGui gui, Item item, float amount, fl click |= gui.BuildFactorioObjectButton(belt, ButtonDisplayStyle.Default) == Click.Left; gui.AllocateSpacing(-1.5f); click |= gui.BuildFactorioObjectButton(inserter, ButtonDisplayStyle.Default) == Click.Left; - text = DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None, "~"); + text = "~" + DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None); if (buildingCount > 1) { text += " (" + DataUtils.FormatAmount(inserterToBelt / buildingCount, UnitOfMeasure.None) + "/b)"; From 3e74bf432a0a32ef5f81197ccc52d399ce629ef5 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:57:33 -0400 Subject: [PATCH 26/54] change: Remove heat icons for better i18n/l10n. --- Yafc/Widgets/ObjectTooltip.cs | 4 +--- Yafc/Windows/ShoppingListScreen.cs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index 9397be84..e51752d1 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -321,9 +321,7 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { using (gui.EnterGroup(contentPadding)) using (gui.EnterRow(0)) { gui.AllocateRect(0, 1.5f); - gui.BuildText($"Requires {DataUtils.FormatAmount(entity.heatingPower, UnitOfMeasure.Megawatt)}"); - gui.BuildFactorioObjectIcon(Database.heat, IconDisplayStyle.Default with { Size = 1.5f }); - gui.BuildText("heat on cold planets."); + gui.BuildText($"Requires {DataUtils.FormatAmount(entity.heatingPower, UnitOfMeasure.Megawatt)} heat on cold planets."); } } diff --git a/Yafc/Windows/ShoppingListScreen.cs b/Yafc/Windows/ShoppingListScreen.cs index dc0e86f5..033d5c02 100644 --- a/Yafc/Windows/ShoppingListScreen.cs +++ b/Yafc/Windows/ShoppingListScreen.cs @@ -142,9 +142,7 @@ public override void Build(ImGui gui) { if (totalHeat > 0) { using (gui.EnterRow(0)) { gui.AllocateRect(0, 1.5f); - gui.BuildText("These entities require " + DataUtils.FormatAmount(totalHeat, UnitOfMeasure.Megawatt)); - gui.BuildFactorioObjectIcon(Database.heat, IconDisplayStyle.Default with { Size = 1.5f }); - gui.BuildText("heat on cold planets."); + gui.BuildText("These entities require " + DataUtils.FormatAmount(totalHeat, UnitOfMeasure.Megawatt) + " heat on cold planets."); } using (gui.EnterRow(0)) { From dde7a32666ea312d7c5ffcc0570f1e67d844d626 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:40:08 -0400 Subject: [PATCH 27/54] change: Remove unused analysis description. --- Yafc.Model/Analysis/Analysis.cs | 2 -- Yafc.Model/Analysis/AutomationAnalysis.cs | 2 -- Yafc.Model/Analysis/CostAnalysis.cs | 4 ---- Yafc.Model/Analysis/Milestones.cs | 3 --- Yafc.Model/Analysis/TechnologyScienceAnalysis.cs | 3 --- 5 files changed, 14 deletions(-) diff --git a/Yafc.Model/Analysis/Analysis.cs b/Yafc.Model/Analysis/Analysis.cs index 21a473ec..82b283c0 100644 --- a/Yafc.Model/Analysis/Analysis.cs +++ b/Yafc.Model/Analysis/Analysis.cs @@ -21,8 +21,6 @@ public static void ProcessAnalyses(IProgress<(string, string)> progress, Project } } - public abstract string description { get; } - public static void Do(Project project) where T : Analysis { foreach (var analysis in analyses) { if (analysis is T t) { diff --git a/Yafc.Model/Analysis/AutomationAnalysis.cs b/Yafc.Model/Analysis/AutomationAnalysis.cs index eda1386b..13aff95f 100644 --- a/Yafc.Model/Analysis/AutomationAnalysis.cs +++ b/Yafc.Model/Analysis/AutomationAnalysis.cs @@ -90,6 +90,4 @@ public override void Compute(Project project, ErrorCollector warnings) { } automatable = state; } - - public override string description => "Automation analysis tries to find what objects can be automated. Object cannot be automated if it requires looting an entity or manual crafting."; } diff --git a/Yafc.Model/Analysis/CostAnalysis.cs b/Yafc.Model/Analysis/CostAnalysis.cs index d207f85f..a6f4821a 100644 --- a/Yafc.Model/Analysis/CostAnalysis.cs +++ b/Yafc.Model/Analysis/CostAnalysis.cs @@ -362,10 +362,6 @@ public override void Compute(Project project, ErrorCollector warnings) { workspaceSolver.Dispose(); } - public override string description => "Cost analysis computes a hypothetical late-game base. This simulation has two very important results: " + - "How much does stuff (items, recipes, etc) cost and how much of stuff do you need. It also collects a bunch of auxiliary results, for example " + - "how efficient are different recipes. These results are used as heuristics and weights for calculations, and are also useful by themselves."; - private static readonly StringBuilder sb = new StringBuilder(); public static string GetDisplayCost(FactorioObject goods) { float cost = goods.Cost(); diff --git a/Yafc.Model/Analysis/Milestones.cs b/Yafc.Model/Analysis/Milestones.cs index 0c3eaa62..a884d562 100644 --- a/Yafc.Model/Analysis/Milestones.cs +++ b/Yafc.Model/Analysis/Milestones.cs @@ -265,7 +265,4 @@ private static bool[] WalkAccessibilityGraph(Project project, HashSet "Milestone analysis starts from objects that are placed on map by the map generator and tries to find all objects that are accessible from that, " + - "taking notes about which objects are locked behind which milestones."; } diff --git a/Yafc.Model/Analysis/TechnologyScienceAnalysis.cs b/Yafc.Model/Analysis/TechnologyScienceAnalysis.cs index 6f29bea4..ad899af0 100644 --- a/Yafc.Model/Analysis/TechnologyScienceAnalysis.cs +++ b/Yafc.Model/Analysis/TechnologyScienceAnalysis.cs @@ -108,7 +108,4 @@ public override void Compute(Project project, ErrorCollector warnings) { allSciencePacks = Database.technologies.CreateMapping( tech => sciencePackCount.Select((x, id) => x[tech] == 0 ? null : new Ingredient(sciencePacks[id], x[tech])).WhereNotNull().ToArray()); } - - public override string description => - "Technology analysis calculates the total amount of science packs required for each technology"; } From 8e8056f286b338145ef670cd7e8d6350bc330ac9 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:33:11 -0400 Subject: [PATCH 28/54] feat(i18n): Move localization work to an I18n library. --- FactorioCalc.sln | 9 ++++ .../FactorioLocalization.cs | 29 ++++++++----- Yafc.I18n/ILocalizable.cs | 7 ++++ .../LocalisedStringParser.cs | 41 ++++++++++--------- Yafc.I18n/Yafc.I18n.csproj | 9 ++++ .../Data/LocalisedStringParserTests.cs | 20 ++++----- Yafc.Model.Tests/Yafc.Model.Tests.csproj | 1 + Yafc.Parser/Data/FactorioDataDeserializer.cs | 11 ++--- Yafc.Parser/FactorioDataSource.cs | 1 + Yafc.Parser/Yafc.Parser.csproj | 1 + 10 files changed, 83 insertions(+), 46 deletions(-) rename {Yafc.Parser => Yafc.I18n}/FactorioLocalization.cs (65%) create mode 100644 Yafc.I18n/ILocalizable.cs rename {Yafc.Parser/Data => Yafc.I18n}/LocalisedStringParser.cs (86%) create mode 100644 Yafc.I18n/Yafc.I18n.csproj diff --git a/FactorioCalc.sln b/FactorioCalc.sln index 248d1484..fa352f30 100644 --- a/FactorioCalc.sln +++ b/FactorioCalc.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution exclusion.dic = exclusion.dic EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.I18n", "Yafc.I18n\Yafc.I18n.csproj", "{4FEC38A5-A997-48C9-97F5-87BD12119F44}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,8 +55,15 @@ Global {66B66728-84F0-4242-B49A-B9D746A3CCA5}.Debug|Any CPU.Build.0 = Debug|Any CPU {66B66728-84F0-4242-B49A-B9D746A3CCA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {66B66728-84F0-4242-B49A-B9D746A3CCA5}.Release|Any CPU.Build.0 = Release|Any CPU + {4FEC38A5-A997-48C9-97F5-87BD12119F44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FEC38A5-A997-48C9-97F5-87BD12119F44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FEC38A5-A997-48C9-97F5-87BD12119F44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FEC38A5-A997-48C9-97F5-87BD12119F44}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {643684AA-6CBA-45BE-A603-BDA3020298A9} + EndGlobalSection EndGlobal diff --git a/Yafc.Parser/FactorioLocalization.cs b/Yafc.I18n/FactorioLocalization.cs similarity index 65% rename from Yafc.Parser/FactorioLocalization.cs rename to Yafc.I18n/FactorioLocalization.cs index 5df039ae..3424c756 100644 --- a/Yafc.Parser/FactorioLocalization.cs +++ b/Yafc.I18n/FactorioLocalization.cs @@ -1,12 +1,18 @@ -using System.Collections.Generic; -using System.IO; +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Yafc.Model.Tests")] -namespace Yafc.Parser; +namespace Yafc.I18n; -internal static class FactorioLocalization { +public static class FactorioLocalization { private static readonly Dictionary keys = []; public static void Parse(Stream stream) { + foreach (var (category, key, value) in Read(stream)) { + keys[$"{category}.{key}"] = CleanupTags(value); + } + } + + public static IEnumerable<(string, string, string)> Read(Stream stream) { using StreamReader reader = new StreamReader(stream); string category = ""; @@ -14,13 +20,15 @@ public static void Parse(Stream stream) { string? line = reader.ReadLine(); if (line == null) { - return; + break; } - line = line.Trim(); + // Trim spaces before keys and all spaces around [categories], but not trailing spaces in values. + line = line.TrimStart(); + string trimmed = line.TrimEnd(); - if (line.StartsWith('[') && line.EndsWith(']')) { - category = line[1..^1]; + if (trimmed.StartsWith('[') && trimmed.EndsWith(']')) { + category = trimmed[1..^1]; } else { int idx = line.IndexOf('='); @@ -31,9 +39,8 @@ public static void Parse(Stream stream) { string key = line[..idx]; string val = line[(idx + 1)..]; - keys[category + "." + key] = CleanupTags(val); + yield return (category, key, val); } - } } @@ -69,7 +76,7 @@ private static string CleanupTags(string source) { return null; } - public static void Initialize(Dictionary newKeys) { + internal static void Initialize(Dictionary newKeys) { keys.Clear(); foreach (var (key, value) in newKeys) { keys[key] = value; diff --git a/Yafc.I18n/ILocalizable.cs b/Yafc.I18n/ILocalizable.cs new file mode 100644 index 00000000..d5eed31e --- /dev/null +++ b/Yafc.I18n/ILocalizable.cs @@ -0,0 +1,7 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Yafc.I18n; + +public interface ILocalizable { + bool Get([NotNullWhen(true)] out string? key, [NotNullWhen(true)] out object[]? parameters); +} diff --git a/Yafc.Parser/Data/LocalisedStringParser.cs b/Yafc.I18n/LocalisedStringParser.cs similarity index 86% rename from Yafc.Parser/Data/LocalisedStringParser.cs rename to Yafc.I18n/LocalisedStringParser.cs index d44f4e64..543b0135 100644 --- a/Yafc.Parser/Data/LocalisedStringParser.cs +++ b/Yafc.I18n/LocalisedStringParser.cs @@ -1,10 +1,9 @@ -using System; -using System.Linq; -using System.Text; +using System.Text; -namespace Yafc.Parser; -internal static class LocalisedStringParser { - public static string? Parse(object localisedString) { +namespace Yafc.I18n; + +public static class LocalisedStringParser { + public static string? ParseObject(object localisedString) { try { return RemoveRichText(ParseStringOrArray(localisedString)); } @@ -13,32 +12,34 @@ internal static class LocalisedStringParser { } } - public static string? Parse(string key, object[] parameters) { + /// + /// Creates the localized string for the supplied key, using for substitutions. + /// + /// The UI key to load. + /// The substitution parameters to be used. + /// The localized string for , using for substitutions. + public static string? ParseKey(string key, object[] parameters) { try { - return RemoveRichText(ParseKey(key, parameters)); + return RemoveRichText(ParseKeyInternal(key, parameters)); } catch { return null; } } - private static string? ParseStringOrArray(object obj) { - if (obj is string str) { - return str; - } - - if (obj is LuaTable table && table.Get(1, out string? key)) { - return ParseKey(key, table.ArrayElements.Skip(1).ToArray()!); + private static string? ParseStringOrArray(object? obj) { + if (obj is ILocalizable table && table.Get(out string? key, out object[]? parameters)) { + return ParseKeyInternal(key, parameters); } - return null; + return obj?.ToString(); } - private static string? ParseKey(string key, object[] parameters) { + private static string? ParseKeyInternal(string key, object?[] parameters) { if (key == "") { StringBuilder builder = new StringBuilder(); - foreach (object subString in parameters) { + foreach (object? subString in parameters) { string? localisedSubString = ParseStringOrArray(subString); if (localisedSubString == null) { return null; @@ -50,7 +51,7 @@ internal static class LocalisedStringParser { return builder.ToString(); } else if (key == "?") { - foreach (object alternative in parameters) { + foreach (object? alternative in parameters) { string? localisedAlternative = ParseStringOrArray(alternative); if (localisedAlternative != null) { return localisedAlternative; @@ -116,7 +117,7 @@ internal static class LocalisedStringParser { case "TILE": case "FLUID": string name = readExtraParameter(); - result.Append(ParseKey($"{type.ToLower()}-name.{name}", [])); + result.Append(ParseKeyInternal($"{type.ToLower()}-name.{name}", [])); break; case "plural_for_parameter": string deciderIdx = readExtraParameter(); diff --git a/Yafc.I18n/Yafc.I18n.csproj b/Yafc.I18n/Yafc.I18n.csproj new file mode 100644 index 00000000..fa71b7ae --- /dev/null +++ b/Yafc.I18n/Yafc.I18n.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Yafc.Model.Tests/Data/LocalisedStringParserTests.cs b/Yafc.Model.Tests/Data/LocalisedStringParserTests.cs index c1312057..45f0b811 100644 --- a/Yafc.Model.Tests/Data/LocalisedStringParserTests.cs +++ b/Yafc.Model.Tests/Data/LocalisedStringParserTests.cs @@ -1,5 +1,5 @@ using Xunit; -using Yafc.Parser; +using Yafc.I18n; namespace Yafc.Model.Data.Tests; @@ -16,49 +16,49 @@ public LocalisedStringParserTests() => FactorioLocalization.Initialize(new Syste [Fact] public void Parse_JustString() { - string localised = LocalisedStringParser.Parse("test"); + string localised = LocalisedStringParser.ParseObject("test"); Assert.Equal("test", localised); } [Fact] public void Parse_RemoveRichText() { - string localised = LocalisedStringParser.Parse("[color=#ffffff]iron[/color] [color=1,0,0]plate[.color] [item=iron-plate]"); + string localised = LocalisedStringParser.ParseObject("[color=#ffffff]iron[/color] [color=1,0,0]plate[.color] [item=iron-plate]"); Assert.Equal("iron plate ", localised); } [Fact] public void Parse_NoParameters() { - string localised = LocalisedStringParser.Parse("not-enough-ingredients", []); + string localised = LocalisedStringParser.ParseKey("not-enough-ingredients", []); Assert.Equal("Not enough ingredients.", localised); } [Fact] public void Parse_Parameter() { - string localised = LocalisedStringParser.Parse("si-unit-kilometer-per-hour", ["100"]); + string localised = LocalisedStringParser.ParseKey("si-unit-kilometer-per-hour", ["100"]); Assert.Equal("100 km/h", localised); } [Fact] public void Parse_LinkItem() { - string localised = LocalisedStringParser.Parse("item-name.big-iron-plate", []); + string localised = LocalisedStringParser.ParseKey("item-name.big-iron-plate", []); Assert.Equal("Big Iron plate", localised); } [Fact] public void Parse_PluralSpecial() { - string localised = LocalisedStringParser.Parse("hours", ["1"]); + string localised = LocalisedStringParser.ParseKey("hours", ["1"]); Assert.Equal("1 hour", localised); } [Fact] public void Parse_PluralRest() { - string localised = LocalisedStringParser.Parse("hours", ["2"]); + string localised = LocalisedStringParser.ParseKey("hours", ["2"]); Assert.Equal("2 hours", localised); } [Fact] public void Parse_PluralWithParameter() { - string localised = LocalisedStringParser.Parse("connecting", ["1"]); + string localised = LocalisedStringParser.ParseKey("connecting", ["1"]); Assert.Equal("1 player is connecting", localised); } @@ -67,7 +67,7 @@ public void Parse_PluralWithParameter() { [InlineData(22, "option 2")] [InlineData(5, "option 3")] public void Parse_PluralEndsIn(int n, string expectedResult) { - string localised = LocalisedStringParser.Parse("ends.in", [n.ToString()]); + string localised = LocalisedStringParser.ParseKey("ends.in", [n.ToString()]); Assert.Equal(expectedResult, localised); } } diff --git a/Yafc.Model.Tests/Yafc.Model.Tests.csproj b/Yafc.Model.Tests/Yafc.Model.Tests.csproj index 0abfb0f1..b4b33c84 100644 --- a/Yafc.Model.Tests/Yafc.Model.Tests.csproj +++ b/Yafc.Model.Tests/Yafc.Model.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/Yafc.Parser/Data/FactorioDataDeserializer.cs b/Yafc.Parser/Data/FactorioDataDeserializer.cs index 2327b131..7ba7e431 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using SDL2; using Serilog; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -425,7 +426,7 @@ void readTrigger(LuaTable table) { item.stackSize = table.Get("stack_size", 1); if (item.locName == null && table.Get("placed_as_equipment_result", out string? result)) { - item.locName = LocalisedStringParser.Parse("equipment-name." + result, [])!; + item.locName = LocalisedStringParser.ParseKey("equipment-name." + result, [])!; } if (table.Get("fuel_value", out string? fuelValue)) { item.fuelValue = ParseEnergy(fuelValue); @@ -712,17 +713,17 @@ private void DeserializeLocation(LuaTable table, ErrorCollector collector) { target.factorioType = table.Get("type", ""); if (table.Get("localised_name", out object? loc)) { // Keep UK spelling for Factorio/LUA data objects - target.locName = LocalisedStringParser.Parse(loc)!; + target.locName = LocalisedStringParser.ParseObject(loc)!; } else { - target.locName = LocalisedStringParser.Parse(prototypeType + "-name." + target.name, [])!; + target.locName = LocalisedStringParser.ParseKey(prototypeType + "-name." + target.name, [])!; } if (table.Get("localised_description", out loc)) { // Keep UK spelling for Factorio/LUA data objects - target.locDescr = LocalisedStringParser.Parse(loc); + target.locDescr = LocalisedStringParser.ParseObject(loc); } else { - target.locDescr = LocalisedStringParser.Parse(prototypeType + "-description." + target.name, []); + target.locDescr = LocalisedStringParser.ParseKey(prototypeType + "-description." + target.name, []); } _ = table.Get("icon_size", out float defaultIconSize); diff --git a/Yafc.Parser/FactorioDataSource.cs b/Yafc.Parser/FactorioDataSource.cs index 8d716b4a..51cd6723 100644 --- a/Yafc.Parser/FactorioDataSource.cs +++ b/Yafc.Parser/FactorioDataSource.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using Serilog; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; diff --git a/Yafc.Parser/Yafc.Parser.csproj b/Yafc.Parser/Yafc.Parser.csproj index 6a400f83..ac0dbd2d 100644 --- a/Yafc.Parser/Yafc.Parser.csproj +++ b/Yafc.Parser/Yafc.Parser.csproj @@ -20,6 +20,7 @@ + From 7deebc2d14ca73570a2fd359c2381ef8b2449eb5 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:34:23 -0400 Subject: [PATCH 29/54] feat(i18n): Add support for __YAFC__ substitutions. --- Yafc.I18n/LocalisedStringParser.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Yafc.I18n/LocalisedStringParser.cs b/Yafc.I18n/LocalisedStringParser.cs index 543b0135..5d1bc9b6 100644 --- a/Yafc.I18n/LocalisedStringParser.cs +++ b/Yafc.I18n/LocalisedStringParser.cs @@ -119,6 +119,10 @@ public static class LocalisedStringParser { string name = readExtraParameter(); result.Append(ParseKeyInternal($"{type.ToLower()}-name.{name}", [])); break; + case "YAFC": + name = readExtraParameter(); + result.Append(ParseKeyInternal("yafc." + name, parameters)); + break; case "plural_for_parameter": string deciderIdx = readExtraParameter(); string? decider = parameters[int.Parse(deciderIdx) - 1]; From e07139420b1032ea5f314441b30ff306fa10e7f3 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Thu, 24 Apr 2025 06:25:00 -0400 Subject: [PATCH 30/54] feat(i18n): Add a localization file. --- Yafc/Data/locale/en/yafc.cfg | 835 +++++++++++++++++++++++++++++++++++ 1 file changed, 835 insertions(+) create mode 100644 Yafc/Data/locale/en/yafc.cfg diff --git a/Yafc/Data/locale/en/yafc.cfg b/Yafc/Data/locale/en/yafc.cfg new file mode 100644 index 00000000..71aeadcc --- /dev/null +++ b/Yafc/Data/locale/en/yafc.cfg @@ -0,0 +1,835 @@ +[yafc] +; Program.cs +yafc-with-version=YAFC CE v__1__ + +; FactorioDataSource.cs +progress-initializing=Initializing +progress-loading-mod-list=Loading mod list +could-not-read-mod-list=Could not read mod list from __1__ +mod-not-found-try-in-factorio=Mod not found: __1__. Try loading this pack in Factorio first. +progress-creating-lua-context=Creating Lua context +circular-mod-dependencies=Mods dependencies are circular. Unable to load mods: __1__ +list-separator=, +progress-completed=Completed! + +; LuaContext.cs +progress-executing-mod-at-data-stage=Executing mods __1__ + +; CommandLineParser.cs +missing-command-line-argument=Missing argument for __1__. +console-help-message=Usage:\nYafc [ [--mods-path ] [--project-file ] [--help]\n\nDescription:\n Yafc can be started without any arguments. However, if arguments are supplied, it is\n mandatory that the first argument is the path to the data directory of Factorio. The\n other arguments are optional in any case.\n\nOptions:\n \n Path of the data directory (mandatory if other arguments are supplied)\n\n --mods-path \n Path of the mods directory (optional)\n\n --project-file \n Path of the project file (optional)\n\n --help\n Display this help message and exit\n\nExamples:\n 1. Starting Yafc without any arguments:\n $ ./Yafc\n This opens the welcome screen.\n\n 2. Starting Yafc with a project path:\n $ ./Yafc path/to/my/project.yafc\n Skips the welcome screen and loads the project. If the project has not been\n opened before, then uses the start-settings of the most-recently-opened project.\n\n 3. Starting Yafc with the path to the data directory of Factorio:\n $ ./Yafc Factorio/data\n This opens a fresh project and loads the game data from the supplied directory.\n Fails if the directory does not exist.\n\n 4. Starting Yafc with the paths to the data directory and a project file:\n $ ./Yafc Factorio/data --project-file my-project.yafc\n This opens the supplied project and loads the game data from the supplied data\n directory. Fails if the directory and/or the project file do not exist.\n\n 5. Starting Yafc with the paths to the data & mods directories and a project file:\n $ ./Yafc Factorio/data --mods-path Factorio/mods --project-file my-project.yafc\n This opens the supplied project and loads the game data and mods from the supplied\n data and mods directories. Fails if any of the directories and/or the project file\n do not exist. +command-line-error-path-does-not-exist=__1__ path '__2__' does not exist. +folder-type-data=Data +folder-type-mods=Mods +folder-type-project=Project +command-line-error-unknown-argument=Unknown argument '__1__'. +command-line-error=Error: __1__ + +; ImmediateWidgets.cs +factorio-object-none=None +see-full-list-button=See full list +clear-button=Clear +per-second-suffix=/s +per-minute-suffix=/m +per-hour-suffix=/h +seconds-per-stack=__1__ per stack +select-quality=Select quality + +; MainScreenTabBar.cs +edit-page-properties=Edit properties +open-secondary-page=Open as secondary +shortcut-ctrl-click=Ctrl+Click +close-secondary-page=Close secondary +duplicate-page=Duplicate page + +; ObjectTooltip.cs +name-with-type=__1__ (__2__) +tooltip-nothing-to-list=Nothing +tooltip-and-more-in-list=... and __1__ more +tooltip-not-accessible=This __1__ is inaccessible, or it is only accessible through mod or map script. Middle-click to open dependency analyzer to investigate. +tooltip-not-automatable=This __1__ cannot be fully automated. This means that it requires either manual crafting, or manual labor such as cutting trees. +tooltip-not-automatable-yet=This __1__ cannot be fully automated at current milestones. +tooltip-has-untranslated-special-type=Special: __1__ +energy-electricity=Power usage: __1__ +energy-heat=Heat energy usage: __1__ +energy-labor=Labor energy usage: __1__ +energy-free=Free energy usage: __1__ +energy-fluid-fuel=Fluid fuel energy usage: __1__ +energy-fluid-heat=Fluid heat energy usage: __1__ +energy-solid-fuel=Solid fuel energy usage: __1__ +tooltip-header-loot=Loot +map-generation-density=Generates on map (estimated density: __1__) +map-generation-density-unknown=Generates on map (density unknown) +entity-crafts=Crafts +entity-crafting-speed=Crafting speed: __1__ +entity-crafting-productivity=Crafting productivity: __1__ +entity-energy-consumption=Energy consumption: __1__ +entity-module-slots=Module slots: __1__ +allowed-module-effects-untranslated-list=Only allowed effects: __1__ +lab-allowed-inputs=Allowed inputs: +perishable=Perishable +tooltip-add-drain-energy=__1__ + __2__ +entity-absorbs-pollution=This building absorbs __1__ +entity-has-high-pollution=This building contributes to global warning! +belt-throughput=Belt throughput (Items): __1__ +inserter-swing-time=Swing time: __1__ +beacon-efficiency=Beacon efficiency: __1__ +accumulator-capacity=Accumulator charge: __1__ +lightning-attractor-extra-info=Power production (average usable): __1__\n Build in a __2__-tile square grid\nProtection range: __3__\nCollection efficiency: __4__ +solar-panel-average-production=Power production (average): __1__ +open-neie-middle-click-hint=Middle mouse button to open Never Enough Items Explorer for this __1__ +tooltip-header-production-recipes=Made with +tooltip-header-miscellaneous-sources=Sources +tooltip-header-consumption-recipes=Needed for +tooltip-header-item-placement-result=Place result +tooltip-header-module-properties=Module parameters +productivity-property=Productivity: __1__ +speed-property=Speed: __1__ +consumption-property=Consumption: __1__ +pollution-property=Pollution: __1__ +quality-property=Quality: __1__ +item-stack-size=Stack size: __1__ +item-rocket-capacity=Rocket capacity: __1__ +analysis-better-recipes-to-create=YAFC analysis: There are better recipes to create __1__. (Wasting __2__% of YAFC cost) +analysis-better-recipes-to-create-all=YAFC analysis: There are better recipes to create each of the products. (Wasting __1__% of YAFC cost) +analysis-wastes-useful-products=YAFC analysis: This recipe wastes useful products. Don't do this recipe. +recipe-uses-fluid-temperature=Uses fluid temperature +recipe-uses-mining-productivity=Uses mining productivity +recipe-production-scales-with-power=Production scaled with power +tooltip-header-recipe-products=Products +tooltip-header-recipe-crafters=Made in +tooltip-header-allowed-modules=Allowed modules +tooltip-header-unlocked-by-technologies=Unlocked by +technology-is-disabled=This technology is disabled and cannot be researched. +tooltip-header-technology-prerequisites=Prerequisites +tooltip-header-technology-item-crafting=Item crafting required +tooltip-header-technology-capture=Capture __plural_for_parameter__1__{1=this|rest=any}__ entity +tooltip-header-technology-mine-entity=Mine __plural_for_parameter__1__{1=this|rest=any}__ entity +tooltip-header-technology-build-entity=Build __plural_for_parameter__1__{1=this|rest=any}__ entity +tooltip-header-unlocks-recipes=Unlocks recipes +tooltip-header-unlocks-locations=Unlocks locations +tooltip-header-total-science-required=Total science required +tooltip-quality-upgrade-chance=Upgrade chance: __1__ (multiplied by module bonus) +tooltip-header-quality-bonuses=Quality bonuses +tooltip-no-normal-bonuses=Normal quality provides no bonuses. +tooltip-quality-crafting-speed=Crafting speed: +tooltip-quality-accumulator-capacity=Accumulator capacity: +tooltip-quality-module-effects=Module effects: +tooltip-quality-beacon-transmission=Beacon transmission efficiency: +tooltip-quality-time-before-spoiling=Time before spoiling: +tooltip-quality-lightning-attractor=Lightning attractor range & efficiency: +quality-bonus-value=+__1__ +quality-bonus-value-with-footnote=+__1__* +tooltip-quality-module-footnote=* Only applied to beneficial module effects. +tooltip-header-technology-launch-item=Launch __plural_for_parameter__1__{1=this|rest=any}__ item +product-suffix-preserved=, preserved until removed from the machine +tooltip-entity-spoils-after-no-production=After __1__ of no production, spoils into +tooltip-entity-expires-after-no-production=Expires after __1__ of no production +tooltip-entity-absorbs-pollution=Absorption: __1__ __2__ per minute +tooltip-entity-emits-pollution=Emission: __1__ __2__ per minute +tooltip-entity-requires-heat=Requires __1__ heat on cold planets. +item-spoils=After __1__, spoils into + +; AboutScreen.cs +about-yafc=About YAFC-CE +full-name=Yet Another Factorio Calculator +about-community-edition=(Community Edition) +about-copyright-shadow=Copyright 2020-2021 ShadowTheAge +about-copyright-community=Copyright 2024 YAFC Community +about-copyleft-gpl-3=This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +about-warranty-disclaimer=This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +about-full-license-text=Full license text: +about-github-page=Github YAFC-CE page and documentation: +about-libraries=Free and open-source third-party libraries used: +about-dot-net-core=Microsoft .NET core and libraries +about-sdl2-libraries=Libraries for SDL2: +about-roboto-font-family=Roboto font family, +about-noto-sans-family=Noto Sans font family, +about-and=and +about-material-design-icon=Material Design Icon collection +about-plus=plus +about-serpent-library=Serpent library +about-and-small-bits=and small bits from +about-factorio-lua-api=Factorio API reference +about-factorio-trademark-disclaimer=Factorio name, content and materials are trademarks and copyrights of Wube Software. +about-factorio-wiki=Documentation on Factorio Wiki + +; DependencyExplorer.cs +dependency-fuel=Fuel +dependency-fuel-missing=There is no fuel to power this entity +dependency-ingredient=Ingredient +dependency-ingredient-missing=There are no ingredients for this recipe +dependency-ingredient-variants-missing=There are no ingredient variants for this recipe +dependency-crafter=Crafter +dependency-crafter-missing=There are no crafters that can craft this item +dependency-source=Source +dependency-sources-missing=This item has no sources +dependency-technology=Research +dependency-technology-missing=This recipe is disabled and there are no technologies to unlock it +dependency-technology-no-prerequisites=There are no technology prerequisites +dependency-item=Item +dependency-item-missing=This entity cannot be placed +dependency-map-source-missing=This recipe requires another entity +dependency-technology-disabled=This technology is disabled +dependency-location=Location +dependency-location-missing=There are no locations that spawn this entity +dependency-require-single=Require this __1__: +dependency-require-all=Require ALL of these __1__s: +dependency-require-any=Require ANY of these __1__s: +; For these two, __1__ is one of the dependency-*-missing messages +dependency-accessible-anyway=__1__, but it is inherently accessible. +dependency-and-not-accessible=__1__, and it is inaccessible. +dependency-explorer=Dependency explorer +dependency-currently-inspecting=Currently inspecting: +dependency-select-something=Select something +dependency-click-to-change-hint=(Click to change) +dependency-automatable=Status: Automatable +dependency-accessible=Status: Accessible, Not automatable +dependency-marked-accessible=Manually marked as accessible +dependency-clear-mark=Clear mark +dependency-mark-not-accessible=Mark as inaccessible +dependency-mark-accessible-ignoring-milestones=Mark as accessible without milestones +dependency-marked-not-accessible=Status: Marked as inaccessible +dependency-not-accessible=Status: Not accessible. Wrong? +dependency-mark-accessible=Manually mark as accessible +dependency-header-dependencies=Dependencies: +dependency-header-dependents=Dependents: + +; ErrorListPanel.cs +error-loading-failed=Loading failed +error-but-loading-succeeded=Loading completed with errors +analysis-warnings=Analysis warnings + +; FilesystemScreen.cs +browser-create-directory=Create directory here + +; ImageSharePanel.cs +sharing-image-generated=Image generated +save-as-png=Save as PNG +save-and-open=Save to temp folder and open +copied-to-clipboard=Copied to clipboard +copy-to-clipboard-with-shortcut=Copy to clipboard (Ctrl+__1__) +save=Save + +; MainScreen.cs +full-name-with-version=Yet Another Factorio Calculator CE v__1__ +create-production-sheet=Create production sheet (Ctrl+__1__) +list-and-search-all=List and search all pages (Ctrl+Shift+__1__) +open-neie=Open NEIE +search-header=Find on page: +undo=Undo +shortcut-ctrl-X=Ctrl+__1__ +save-as=Save As +find-on-page=Find on page +load-with-same-mods=Load another project (Same mods) +return-to-welcome-screen=Return to starting screen +menu-header-tools=Tools +milestones=Milestones +menu-preferences=Preferences +menu-summary=Summary +menu-legacy-summary=Summary (Legacy) +neie=Never Enough Items Explorer +open-dependency-explorer=Open Dependency Explorer +import-from-clipboard=Import page from clipboard +menu-header-extra=Extra +menu-run-factorio=Run Factorio +menu-check-for-updates=Check for updates +menu-about=About YAFC +alert-unsaved-changes=You have __1__ unsaved changes +alert-unsaved-changes-in-file=You have __1__ unsaved changes to __2__ +query-save-changes=Save unsaved changes? +dont-save=Don't save +new-version-available=New version available! +new-version-number=There is a new version available: __1__ +visit-release-page=Visit release page +close=Close +no-newer-version=No newer version +running-latest-version=You are running the latest version! +network-error=Network error +error-while-checking-for-new-version=There were an error while checking versions. +save-project-window-title=Save project +save-project-window-header=Save project as +load-project-window-title=Load project +load-project-window-header=Load another .yafc project +select-project=Select +error-critical-loading-exception=Critical loading exception +default-file-name=project + +; MainScreen.PageListSearch.cs +search-all-header=Search in: +search-all-location-page-name=Page name +search-all-location-outputs=Desired products +search-all-location-recipes=Recipes +search-all-location-inputs=Ingredients +search-all-location-extra-outputs=Extra products +search-all-location-all=All +search-all-localized-strings=Localized names +search-all-internal-strings=Internal names +search-all-both-strings=Both +search-all-middle-mouse-to-edit-hint=Middle mouse button to edit + +; MilestonesEditor.cs +milestone-editor=Milestone editor +milestone-description=Hint: You can reorder milestones. When an object is locked behind a milestone, the first inaccessible milestone will be shown. Also when there is a choice between different milestones, first will be chosen +milestone-auto-sort=Auto sort milestones +milestone-add=Add milestone +milestone-add-new=Add new milestone +milestone-cannot-add=Cannot add milestone +milestone-cannot-add-already-exists=Milestone already exists + +; MilestonesPanel.cs +milestones-header=Please select objects that you already have access to: +milestones-description=For your convenience, YAFC will show objects you DON'T have access to based on this selection.\nThese are called 'Milestones'. By default all science packs and locations are added as milestones, but this does not have to be this way! You can define your own milestones: Any item, recipe, entity or technology may be added as a milestone. For example you can add advanced electronic circuits as a milestone, and YAFC will display everything that is locked behind those circuits +milestones-edit=Edit milestones +milestones-edit-settings=Edit tech progression settings +done=Done + +; NeverEnoughItemsPanel.cs +neie-accepted-variants=Accepted fluid variants +neie-building-hours-suffix=__1__bh +neie-building-hours-description=Building-hours.\nAmount of building-hours required for all researches assuming crafting speed of 1. +fuel-value-can-be-used=Fuel value __1__ can be used for: +fuel-value-zero-can-be-used=Can be used to fuel: +neie-show-special-recipes=Show special recipes (barreling / voiding) +neie-show-locked-recipes=There are more recipes, but they are locked based on current milestones. +neie-show-inaccessible-recipes=There are more recipes, but they are inaccessible. +neie-show-more-recipes=Show more recipes +neie-special-recipes=Special recipes: +neie-locked-recipes=Locked recipes: +neie-inaccessible-recipes=Inaccessible recipes: +neie-hide-recipes=hide +neie-header=Never Enough Items Explorer +select-item=Select item +neie-production=Production: +neie-usage=Usages: +neie-colored-bars-link=What do colored bars mean? +neie-how-to-read-colored-bars=How to read colored bars +neie-colored-bars-description=Blue bar means estimated production or consumption of the thing you selected. Blue bar at 50% means that that recipe produces(consumes) 50% of the product.\n\nOrange bar means estimated recipe efficiency. If it is not full, the recipe looks inefficient to YAFC.\n\nIt is possible for a recipe to be efficient but not useful - for example a recipe that produces something that is not useful.\n\nYAFC only estimates things that are required for science recipes. So buildings, belts, weapons, fuel - are not shown in estimations. +neie-current-milestones-checkbox=Current milestones info + +; PreferencesScreen.cs +preferences-tab-general=General +preferences-tab-progression=Progression +preferences=Preferences +preferences-default-belt=Default belt: +preferences-default-inserter=Default inserter: +preferences-inserter-capacity=Inserter capacity: +preferences-target-technology=Target technology for cost analysis: +preferences-mining-productivity-bonus=Mining productivity bonus: +preferences-research-speed-bonus=Research speed bonus: +preferences-research-productivity-bonus=Research productivity bonus: +preferences-technology-level=__1__ Level: +prefs-unit-of-time=Unit of time: +prefs-unit-seconds=Second +prefs-unit-minutes=Minute +prefs-time-unit-hours=Hour +prefs-time-unit-custom=Custom +prefs-header-item-units=Item production/consumption: +prefs-header-fluid-units=Fluid production/consumption: +prefs-pollution-cost-hint=0% for off, 100% for old default +prefs-pollution-cost=Pollution cost modifier +prefs-icon-scale-hint=Some mod icons have little or no transparency, hiding the background color. This setting reduces the size of icons that could hide link information. +prefs-icon-scale=Display scale for linkable icons +prefs-milestones-per-line-hint=Some tooltips may want to show multiple rows of milestones. Increasing this number will draw fewer lines in some tooltips, by forcing the milestones to overlap.\n\nMinimum: 22\nDefault: 28 +prefs-milestones-per-line=Maximum milestones per line in tooltips: +prefs-reactor-layout=Reactor layout: +prefs-spoiling-rate-hint=Set this to match the spoiling rate you selected when starting your game. 10% is slow spoiling, and 1000% (1k%) is fast spoiling. +prefs-spoiling-rate=Spoiling rate: +prefs-show-inaccessible-milestone-overlays=Show milestone overlays on inaccessible objects +prefs-dark-mode=Dark mode +prefs-autosave=Enable autosave (Saves when the window loses focus) +prefs-goods-unit-simple=Simple Amount__1__ +prefs-goods-unit-custom=Custom: 1 unit equals +prefs-goods-unit-from-belt=Set from belt +prefs-select-belt=Select belt +per-second-suffix-long=per second +prefs-reactor-x-y-separator=x + +; ProjectPageSettingsPanel.cs +page-settings-name-hint=Input name +select-icon=Select icon +page-settings-icon-hint=And select icon +page-settings-create-header=Create new page +page-settings-edit-header=Edit page icon and name +create=Create +ok=OK +cancel=Cancel +page-settings-other=Other tools +delete-page=Delete page +export-page-to-clipboard=Share (export string to clipboard) +page-settings-screenshot=Make full page screenshot +page-settings-export-calculations=Export calculations (to clipboard) +alert-import-page-newer-version=String was created with the newer version of YAFC (__1__). Data may be lost. +alert-import-page-incompatible-version=Share string was created with future version of YAFC (__1__) and is incompatible. +alert-import-page-invalid-string=Clipboard text does not contain valid YAFC share string. +import-page-already-exists=Page already exists +import-page-already-exists-long=Looks like this page already exists with name '__1__'. Would you like to replace it or import as a copy? +replace=Replace +import-as-copy=Import as copy +export-no-fuel-selected=No fuel selected +export-recipe-disabled=Recipe disabled + +; SelectMultiObjectPanel.cs +select-multiple-objects-hint=Hint: ctrl+click to select multiple + +; SelectObjectPanel.cs +type-for-search-hint=Start typing for search + +; ShoppingListScreen.cs +shopping-list-count-of-items=x__1__: __2__ +shopping-list-total-buildings=Total buildings +shopping-list-total-buildings-hint=Display the total number of buildings required, ignoring the built building count. +shopping-list-built-buildings=Built buildings +shopping-list-built-buildings-hint=Display the number of buildings that are reported in built building count. +shopping-list-missing-buildings=Missing buildings +shopping-list-missing-buildings-hint=Display the number of additional buildings that need to be built. +shopping-list-assume-no-buildings=No buildings +shopping-list-assume-no-buildings-hint=When the built building count is not specified, behave as if it was set to 0. +shopping-list-assume-enough-buildings=Enough buildings +shopping-list-assume-enough-buildings-hint=When the built building count is not specified, behave as if it matches the required building count. +shopping-list=Shopping list +shopping-list-cost-information=Total cost of all objects: ¥__1__, buildings: __2__, modules: __3__ +shopping-list-building-assumption-header=When not specified, assume: +shopping-list-allow-additional-heat=Allow additional heat for +shopping-list-heat-for-inserters=inserters +shopping-list-heat-for-pipes=pipes +shopping-list-heat-for-belts=belts +shopping-list-heat-and=, and +shopping-list-heat-for-other-entities=other entities +shopping-list-heat-period=. +shopping-list-decompose=Decompose +shopping-list-export-blueprint=Export to blueprint +shopping-list-export-blueprint-hint=Blueprint string will be copied to the clipboard. +shopping-list-these-require-heat=These entities require __1__ heat on cold planets. + +; WelcomeScreen.cs +welcome-project-file-location=Project file location +welcome-project-file-location-hint=You can leave it empty for a new project +welcome-data-location=Factorio Data location*\nIt should contain folders 'base' and 'core' +welcome-data-location-hint=e.g. C:/Games/Steam/SteamApps/common/Factorio/data +welcome-mod-location=Factorio Mods location (optional)\nIt should contain file 'mod-list.json' +welcome-mod-location-hint=If you don't use separate mod folder, leave it empty +; Possibly translate this as just "Language:"? +welcome-language-header=In-game objects language: +welcome-load-autosave=Load most recent (auto-)save +welcome-use-net-production=Use net production/consumption when analyzing recipes +welcome-software-render-hint=If checked, the main project screen will not use hardware-accelerated rendering.\n\nEnable this setting if YAFC crashes after loading without an error message, or if you know that your computer's graphics hardware does not support modern APIs (e.g. DirectX 12 on Windows). +welcome-software-render=Force software rendering in project screen +recent-projects=Recent projects +toggle-dark-mode=Toggle dark mode +load-error-create-issue=If all else fails, then create an issue on GitHub +load-error-create-issue-with-information=Please attach a new-game save file to sync mods, versions, and settings. +welcome-alert-download-font=Yafc will download a suitable font before it restarts.\nThis may take a minute or two. +confirm=Confirm +welcome-alert-english-fallback=Mods may not support your language, using English as a fallback. +welcome-alert-need-a-different-font=These languages are not supported by the current font. Click the language to restart with a suitable font, or click '__YAFC__welcome-select-font__' to select a custom font. +welcome-select-font=Select font +welcome-reset-font=Reset font to default +welcome-reset-font-restart=Restart Yafc to switch to the selected font. +override-font=Override font +override-font-long=Override the font that YAFC uses +load-project-name=Load '__1__' +welcome-alert-missing-directory=Project directory does not exist +welcome-create-project-name=Create '__1__' +welcome-create-unnamed-project=Create new project +welcome-browse-button=... +select=Select +select-folder=Select folder +disable-and-reload=Disable & reload +disable-and-reload-hint=Disable this mod until you close YAFC or change the mod folder. +welcome=Welcome to YAFC CE v__1__ +please-wait=Please wait . . . +downloading-fonts=YAFC is downloading the fonts for your language. +unable-to-load-with-mod=YAFC was unable to load the project. You can disable the problematic mod once by clicking on the '__YAFC__disable-and-reload__' button, or you can disable it permanently for YAFC by copying the mod-folder, disabling the mod in the copy by editing mod-list.json, and pointing YAFC to the copy. +unable-to-load=YAFC cannot proceed because it was unable to load the project. +copy-to-clipboard=Copy to clipboard +error-while-loading-mod=Error while loading mod __1__. +more-info=More info +back-button=Back +load-error-advice=Check that these mods load in Factorio.\nYAFC only supports loading mods that were loaded in Factorio before. If you add or remove mods or change startup settings, you need to load those in Factorio and then close the game because Factorio saves mod-list.json only when exiting.\nCheck that Factorio loads mods from the same folder as YAFC.\nIf that doesn't help, try removing the mods that have several versions, or are disabled, or don't have the required dependencies. + +; WizardPanel.cs +wizard-finish=Finish +wizard-next=Next +wizard-previous=Previous +wizard-step-X-of-Y=Step __1__ of __2__ + +; AutoPlannerView.cs +auto-planner=Auto planner +auto-planner-warning=This is an experimental feature and may lack functionality. Unfortunately, after some prototyping it wasn't very useful to work with. More research required. +auto-planner-page-name=Enter page name: +auto-planner-goal=Select your goal: +auto-planner-select-production-goal=New production goal +auto-planner-review-milestones=Review active milestones, as they will restrict recipes that are considered: + +; SummaryView.cs +page=Page +summary-column-linked=Linked +summary-header=Production Sheet Summary +summary-only-show-issues=Only show issues +summary-auto-balance-hint=Attempt to match production and consumption of all linked products on the displayed pages.\n\nYou will often have to click this button multiple times to fully balance production. +summary-auto-balance=Auto balance + +; Analysis.cs +progress-running-analysis=Running analysis algorithms + +; CostAnalysis.cs +cost-analysis-estimated-amount-for=Estimated amount for __1__: +cost-analysis-estimated-amount=Estimated amount for all researches: +cost-analysis-failed=Cost analysis was unable to process this modpack. This may indicate a bug in Yafc. +analysis-not-automatable=YAFC analysis: Unable to find a way to fully automate this. +cost-analysis-fluid-cost=YAFC cost per 50 units of fluid: ¥__1__ +cost-analysis-item-cost=YAFC cost per item: ¥__1__ +cost-analysis-energy-cost=YAFC cost per 1 MW: ¥__1__ +cost-analysis-recipe-cost=YAFC cost per recipe: ¥__1__ +cost-analysis-generic-cost=YAFC cost: ¥__1__ +cost-analysis-with-current-cost=__1__ (Currently ¥__2__) + +; DependencyNode.cs +dependency-or-bar=-- OR -- + +; DataClasses.cs +ingredient-amount=__1__x __2__ +ingredient-amount-with-temperature=__1__x __2__ (__3__) +product-amount=__1__x __2__ +product-amount-range=__1__-__2__x __3__ +product-probability=__1__ __2__ +product-probability-amount=__1__ __2__x __3__ +product-probability-amount-range=__1__ __2__-__3__x __4__ +; Appended to the one of the above +product-always-fresh=__1__, always fresh +product-fixed-spoilage=__1__, __2__ spoiled + +temperature=__1__° +temperature-range=__1__°-__2__° + +; DataUtils.cs +ctrl-click-hint-complete-milestones=Hint: Complete milestones to enable ctrl+click +ctrl-click-hint-mark-accessible=Hint: Mark a recipe as accessible to enable ctrl+click +ctrl-click-hint-will-add-favorite=Hint: Ctrl+click to add your favorited recipe +ctrl-click-hint-multiple-favorites=Hint: Cannot ctrl+click with multiple favorited recipes +ctrl-click-hint-will-add-normal=Hint: Ctrl+click to add the accessible normal recipe +ctrl-click-hint-will-add-special=Hint: Ctrl+click to add the accessible recipe +ctrl-click-hint-set-favorite=Hint: Set a favorite recipe to add it with ctrl+click +format-time-in-seconds=__1__ seconds +format-time-in-minutes=__1__ minutes +format-time-in-hours=__1__ hours + +; AutoPlanner.cs +auto-planner-missing-goal=Auto planner goal no longer exist +auto-planner-no-solution=Model has no solution + +; ProductionSummary.cs +legacy-summary-page-missing=Page missing +legacy-summary-broken-entry=Broken entry +load-error-referenced-page-not-found=Referenced page does not exist +load-error-object-does-not-exist=Object does not exist + +; ProductionTable.cs +production-table-no-solution-and-no-deadlocks=YAFC failed to solve the model and to find deadlock loops. As a result, the model was not updated. +production-table-numerical-errors=This model has numerical errors (probably too small or too large numbers) and cannot be solved. +production-table-unexpected-error=Unaccounted error: MODEL___1__ +production-table-requires-more-buildings=This model requires more buildings than are currently built. + +; ProductionTableContent.cs +link-warning-no-production=This link has no production. (Link ignored) +link-warning-no-consumption=This link has no consumption. (Link ignored) +link-warning-unmatched-nested-link=Nested table link has unmatched production/consumption. These unmatched products are not captured by this link. +link-message-remove-to-link-with-parent=Nested tables have their own set of links that DON'T connect to parent links. To connect this product to the outside, remove this link. +link-warning-negative-feedback=YAFC was unable to satisfy this link (Negative feedback loop). This doesn't mean that this link is the problem, but it is part of the loop. +link-warning-needs-overproduction=YAFC was unable to satisfy this link (Overproduction). You can allow overproduction for this link to solve the error. +load-error-recipe-does-not-exist=Recipe does not exist +load-error-linked-product-does-not-exist=Linked product does not exist + +; Project.cs +error-loading-autosave=Fatal error reading the latest autosave. Loading the base file instead. +load-error-did-not-read-all-data=Json was not consumed to the end! +load-warning-newer-version=This file was created with future YAFC version. This may lose data. +load-error-unable-to-load-file=Unable to load project file + +; ErrorCollector.cs +repeated-error=__1__ (x__2__) + +; PropertySerializers.cs +load-warning-unexpected-object=Project contained an unexpected object. + +; SerializationMap.cs +load-error-unable-to-deserialize-untranslated=Unable to deserialize __1__ +load-error-encountered-unexpected-value=Encountered an unexpected value when reading the project file. + +; ValueSerializers.cs +load-error-untranslated-type-does-not-exist=Type __1__ does not exist. Possible plugin version change. +load-error-fluid-has-incorrect-temperature=Fluid __1__ doesn't have correct temperature information. May require adjusting its temperature. +load-error-untranslated-factorio-object-not-found=Factorio object '__1__' no longer exists. Check mods configuration. +load-error-untranslated-factorio-quality-not-found=Factorio quality '__1__' no longer exists. Check mods configuration. + +; FactorioDataDeserializer.cs +progress-loading=Loading +progress-loading-items=Loading items +progress-loading-tiles=Loading tiles +progress-loading-fluids=Loading fluids +progress-loading-recipes=Loading recipes +progress-loading-locations=Loading locations +progress-loading-technologies=Loading technologies +progress-loading-qualities=Loading qualities +progress-loading-entities=Loading entities +progress-postprocessing=Post-processing +progress-computing-maps=Computing maps +progress-calculating-dependencies=Calculating dependencies +progress-creating-project=Creating project +progress-rendering-icons=Rendering icons +progress-rendering-X-of-Y=__1__/__2__ +progress-building-objects=Building objects + +special-recipe-launched=__1__ launched +special-recipe-generating=__1__ generating +special-recipe-launch=__1__ launch +special-recipe-boiling=__1__ boiling to __2__° +special-recipe-pumping=__1__ pumping +special-recipe-mining=__1__ mining +special-recipe-planting=__1__ planting +special-recipe-spoiling=__1__ spoiling + +; FactorioDataDeserializer_Context.cs +special-object-electricity=Electricity +special-object-electricity-description=This is an object that represents electric energy +special-object-heat=Heat +special-object-heat-description=This is an object that represents heat energy +special-object-void=Void +special-object-void-description=This is an object that represents infinite energy +special-object-launch-slot=Rocket launch slot +special-object-launch-slot-description=This is a slot in a rocket ready to be launched +special-item-total-consumption=Total item consumption +special-item-total-consumption-description=This item represents the combined total item input of a multi-ingredient recipe. It can be used to set or measure the number of sushi belts required to supply this recipe row. +special-item-total-production=Total item production +special-item-total-production-description=This item represents the combined total item output of a multi-product recipe. It can be used to set or measure the number of sushi belts required to handle the products of this recipe row. +localization-fallback-description-recipe-to-create=A recipe to create __1__ +localization-fallback-description-item-to-build=An item to build __1__ +fluid-name-with-temperature=__1__ __2__° +fluid-description-temperature-solo=Temperature: __1__° +fluid-description-temperature-added=Temperature: __1__°\n__2__ + +; FactorioDataDeserializer_RecipeAndTechnology.cs +research-has-an-unsupported-trigger-type=Research trigger of __1__ has an unsupported type __2__ + +; Milestones.cs +milestone-analysis-most-inaccessible=More than 50% of all in-game objects appear to be inaccessible in this project with your current mod list. This can have a variety of reasons like objects being accessible via scripts__YAFC__milestone-analysis-or-bug__ __YAFC__milestone-analysis-is-important__ +milestone-analysis-no-rocket-launch=Rocket launch appears to be inaccessible. This means that rocket may not be launched in this mod pack, or it requires mod script to spawn or unlock some items__YAFC__milestone-analysis-or-bug__ __YAFC__milestone-analysis-is-important__ +milestone-analysis-inaccessible-milestones=There are some milestones that are not accessible: __1__. You may remove these from milestone list__YAFC__milestone-analysis-or-bug__ __YAFC__milestone-analysis-is-important__ +milestone-analysis-or-bug=, or it might be due to a bug inside a mod or YAFC. +milestone-analysis-is-important=A lot of YAFC's systems rely on objects being accessible, so some features may not work as intended.\n\nFor this reason YAFC has a Dependency Explorer that allows you to manually enable some of the core recipes. YAFC will iteratively try to unlock all the dependencies after each recipe you manually enabled. For most modpacks it's enough to unlock a few early recipes like any special recipes for plates that everything in the mod is based on. + +; ImGuiUtils.cs +search-hint=Search + +; ProductionSummaryView.cs +legacy-summary-group-name-hint=Group name +legacy-summary-go-to-page=Go to page +remove=Remove +legacy-summary-other-column=Other +legacy-summary-empty-group=This is an empty group +legacy-summary-empty-group-description=Add your existing sheets here to keep track of what you have in your base and to see what shortages you may have. +legacy-summary-group-description=List of goods produced/consumed by added blocks. Click on any of these to add it to (or remove it from) the table. +legacy-summary-multiplier-edit-box-prefix=x + +; ModuleCustomizationScreen.cs +module-customization=Module customization +module-customization-name-hint=Enter name +module-customization-filter-buildings=Filter by crafting buildings (Optional): +module-customization-add-filter-building=Add module template filter +module-customization-enable=Enable custom modules +module-customization-internal-modules=Internal modules: +module-customization-leave-zero-hint=Leave zero amount to fill the remaining slots +module-customization-beacons-only=This building doesn't have module slots, but can be affected by beacons +module-customization-beacon-modules=Beacon modules: +module-customization-using-default-beacons=Use default parameters +module-customization-override-beacons=Override beacons as well +module-customization-use-number-of-modules-in-beacons=Input the amount of modules, not the amount of beacons. Single beacon can hold __1__ modules. +module-customization-current-effects=Current effects: +module-customization-productivity-bonus=Productivity bonus: __1__ +module-customization-speed-bonus=Speed bonus: __1__ (Crafting speed: __2__) +module-customization-quality-bonus=Quality bonus: __1__ (multiplied by quality upgrade chance) +module-customization-energy-usage=Energy usage: __1__ +module-customization-overall-speed=Overall crafting speed (including productivity): __1__ +module-customization-energy-cost-per-output=Energy cost per recipe output: __1__ +module-customization-energy-usage-per-building=Energy usage: __1__ (__2__ per building) +partial-cancel=Cancel (partial) +module-customization-remove=Remove module customization +select-beacon=Select beacon +select-module=Select module + +; ModuleFillerParametersScreen.cs +affected-by-N-beacons=Affected by __1__ +each-containing-N-modules=each containing __1__ +module-filler-remove-current-override-hint=Click here to remove the current override. +select-beacon-module=Select beacon module +use-no-modules=Use no modules +use-best-modules=Use best modules +module-filler-payback-estimate=Modules payback estimate: __1__ +module-filler-header-autofill=Module autofill parameters +module-filler-fill-miners=Fill modules in miners +module-filler-module=Filler module: +module-filler-module-hint=Use this module when autofill doesn't add anything (for example when productivity modules doesn't fit) +module-filler-select-module=Select filler module +module-filler-header-beacons=Beacons & beacon modules: +module-filler-no-beacons=Your mods contain no beacons, or no modules that can be put into beacons. +module-filler-select-beacon-module=Select module for beacon +module-filler-beacons-per-building=Beacons per building: +module-filler-beacons-not-calculated=Please note that beacons themselves are not part of the calculation. +module-filler-override-beacons=Override beacons: +module-filler-override-beacons-hint=Click to change beacon, right-click to change module\nSelect the 'none' item in either prompt to remove the override. +module-filler-add-beacon-override=Add an override for a building type +module-filler-select-overridden-crafter=Add exception(s) for: + +; ModuleTemplateConfiguration.cs +module-templates=Module templates +create-new-template-hint=Create new template + +; ProductionLinkSummaryScreen.cs +link-summary-production=Production: __1__ +link-summary-implicit-links=Plus additional production from implicit links +link-summary-consumption=Consumption: __1__ +link-summary-child-links=Child links: +link-summary-parent-links=Parent links: +link-summary-unrelated-links=Unrelated links: +link-summary-unlinked=Unlinked: +link-summary=Link summary +link-summary-header=Exploring link for: +remove-link=Remove link +link-summary-no-products=This recipe has no linked products. +link-summary-no-ingredients=This recipe has no linked ingredients +link-summary-select-product=Select product link to inspect +link-summary-select-ingredient=Select ingredient link to inspect +link-summary-requested-production=Requested production: __1__ +link-summary-requested-consumption=Requested consumption: __1__ +link-summary-overproduction=Overproduction: __1__ +link-summary-overconsumption=overconsumption: __1__ +link-summary-link-nested-under=This link is nested under: +link-summary-recipe-nested-under=This recipe is nested under: + +; ProductionTableView.cs +production-table-nested-group=This is a nested group. You can drag&drop recipes here. Nested groups can have their own linked materials. +production-table-header-recipe=Recipe +production-table-create-nested=Create nested table +production-table-add-nested-product=Add nested desired product +production-table-unpack-nested=Unpack nested table +production-table-shortcut-right-click=Shortcut: right-click +production-table-shortcut-expand-and-right-click=Shortcut: Expand, then right-click +production-table-show-total-io=Show total Input/Output +enabled=Enabled +add-recipe-to-favorites=Add recipe to favorites +production-table-delete-nested=Delete nested table +production-table-shortcut-collapse-and-right-click=Shortcut: Collapse, then right-click +production-table-delete-recipe=Delete recipe +production-table-export-to-blueprint=Export inputs and outputs to blueprint with constant combinators: +export-blueprint-amount-per=Amount per: +export-blueprint-amount-per-second=second +export-blueprint-amount-per-minute=minute +export-blueprint-amount-per-hour=hour +production-table-remove-zero-building-recipes=Remove all zero-building recipes +production-table-clear-recipes=Clear recipes +production-table-add-all-recipes=Add ALL recipes +production-table-add-raw-recipe=Add raw recipe +production-table-add-technology-hint=Ctrl-click to add a technology instead +select-technology=Select technology +production-table-select-raw-recipe=Select raw recipe +production-table-header-entity=Entity +select-accumulator=Select accumulator +select-crafting-entity=Select crafting entity +production-table-clear-fixed-multiplier=Clear fixed recipe multiplier +production-table-fixed-buildings-hint=Tell YAFC how many buildings it must use when solving this page.\nUse this to ask questions like 'What does it take to handle the output of ten miners?' +production-table-clear-fixed-building-count=Clear fixed building count +production-table-set-fixed-building-count=Set fixed building count +production-table-built-building-count-hint=Tell YAFC how many of these buildings you have in your factory.\nYAFC will warn you if you need to build more buildings. +production-table-clear-built-building-count=Clear built building count +production-table-set-built-building-count=Set built building count +production-table-generate-building-blueprint-hint=Generate a blueprint for one of these buildings, with the recipe and internal modules set. +production-table-generate-building-blueprint=Create single building blueprint +production-table-add-building-to-favorites=Add building to favorites +production-table-mass-set-assembler=Mass set assembler +production-table-select-mass-assembler=Set assembler for all recipes +production-table-mass-set-quality=Mass set quality +production-table-mass-set-fuel=Mass set fuel +production-table-select-mass-fuel=Set fuel for all recipes +production-table-header-ingredients=Ingredients +production-table-header-products=Products +production-table-output-preserved-in-machine=This recipe output does not start spoiling until removed from the machine. +production-table-output-always-fresh=This recipe output is always fresh. +production-table-output-fixed-spoilage=This recipe output is __1__ spoiled. +production-table-header-modules=Modules +production-table-module-template-incompatible=This module template seems incompatible with the recipe or the building +production-table-use-default-modules=Use default modules +production-table-select-modules=Select fixed module +production-table-use-module-template=Use module template: +production-table-configure-module-templates=Configure module templates +production-table-customize-modules=Customize modules +production-table-auto-modules=Auto modules +production-table-module-settings=Module settings +favorite=Favorite +production-table-alert-recipe-exists=Recipe already exists +production-table-query-add-copy=Add a second copy of __1__? +production-table-add-copy=Add a copy +production-table-alert-no-known-fuels=This entity has no known fuels +production-table-add-fuel-to-favorites=Add fuel to favorites +production-table-select-fuel=Select fuel +production-table-accepted-fluids=Accepted fluid variants: +production-table-add-technology=Add technology +production-table-add-production-recipe=Add production recipe +production-table-create-new-table=Create new production table for __1__ +production-table-produce-as-spent-fuel=Produce it as a spent fuel +production-table-add-consumption-recipe=Add consumption recipe +production-table-add-fuel-usage=Add fuel usage +production-table-add-consumption-technology=Add consumption technology +production-table-add-multiple-hint=Hint: ctrl+click to add multiple +production-table-allow-overproduction=Allow overproduction +production-table-view-link-summary=View link summary +production-table-cannot-unlink=__1__ is a desired product and cannot be unlinked. +production-table-currently-linked=__1__ production is currently linked. This means that YAFC will try to match production with consumption. +production-table-remove-desired-product=Remove desired product +production-table-remove-and-unlink-desired-product=Remove and unlink +production-table-unlink=Unlink +production-table-linked-in-parent=__1__ production is currently linked, but the link is outside this nested table. Nested tables can have their own separate links. +production-table-create-link=Create link +production-table-not-linked=__1__ production is currently NOT linked. This means that YAFC will make no attempt to match production with consumption. +production-table-implicitly-linked=__1__ (__2__) production is implicitly linked. This means that YAFC will use it, along with all other available qualities, to produce __3__.\nYou may add a regular link to replace this implicit link. +production-table-set-fixed-fuel=Set fixed fuel consumption +production-table-set-fixed-ingredient=Set fixed ingredient consumption +production-table-set-fixed-product=Set fixed production amount +production-table-set-fixed-will-replace=This will replace the other fixed amount in this row. +production-table-clear-fixed-fuel=Clear fixed fuel consumption +production-table-clear-fixed-ingredient=Clear fixed ingredient consumption +production-table-clear-fixed-product=Clear fixed production amount +production-table-buildings-per-half-belt=(Buildings per half belt: __1__) +production-table-inserters-per-building=__1__ (__2__/building) +production-table-approximate-number=~__1__ +production-table-approximate-inserters-per-building=~__1__ (__2__/b) +warning-description-deadlock-candidate=Contains recursive links that cannot be matched. No solution exists. +warning-description-overproduction-required=This model cannot be solved exactly, it requires some overproduction. You can allow overproduction for any link. This recipe contains one of the possible candidates. +warning-description-entity-not-specified=Crafter not specified. Solution is inaccurate. +warning-description-fuel-not-specified=Fuel not specified. Solution is inaccurate. +warning-description-fluid-with-temperature=This recipe uses fuel with temperature. Should link with producing entity to determine temperature. +warning-description-fluid-too-hot=Fluid temperature is higher than generator maximum. Some energy is wasted. +warning-description-fuel-does-not-provide-energy=This fuel cannot provide any energy to this building. The building won't work. +warning-description-has-max-fuel-consumption=This building has max fuel consumption. The rate at which it works is limited by it. +warning-description-ingredient-temperature-range=This recipe does care about ingredient temperature, and the temperature range does not match +warning-description-assumes-reactor-formation=Assumes reactor formation from preferences. __YAFC__warning-description-click-for-preferences__ +warning-description-assumes-nauvis-solar=Energy production values assumes Nauvis solar ration (70% power output). Don't forget accumulators. +warning-description-needs-more-buildings=This recipe requires more buildings than are currently built. +warning-description-asteroid-collectors=The speed of asteroid collectors depends heavily on location and travel speed. It also depends on the distance between adjacent collectors. These dependencies are not modeled. Expect widely varied performance. +warning-description-assumes-fulgoran-lightning=Energy production values assume Fulgoran storms and attractors in a square grid.\nThe accumulator estimate tries to store 10% of the energy captured by the attractors. +warning-description-useless-quality=The quality bonus on this recipe has no effect. Make sure the recipe produces items and that all milestones for the next quality are unlocked. __YAFC__warning-description-click-for-milestones__ +warning-description-excess-productivity-bonus=This building has a larger productivity bonus (from base effect, research, and/or modules) than allowed by the recipe. Please make sure you entered productivity research levels, not percent bonuses. __YAFC__warning-description-click-for-preferences__ +warning-description-click-for-preferences=(Click to open the preferences) +warning-description-click-for-milestones=(Click to open the milestones window) +production-table-add-desired-product=Add desired product +production-table-desired-products=Desired products and amounts (Use negative for input goal): +production-table-summary-ingredients=Summary ingredients: +production-table-import-ingredients=Import ingredients: +production-table-extra-products=Extra products: +production-table-export-products=Export products: + +; ProjectPage.cs +default-new-page-name=New page + +; ExceptionScreen.cs +ignore-future-errors=Ignore future errors From cc9dfa2589174a46ce91c306a7aa67a058fa3067 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:37:15 -0400 Subject: [PATCH 31/54] feat(i18n): Generate identifiers for localization keys. --- .gitignore | 1 + FactorioCalc.sln | 9 + Yafc.I18n.Generator/SourceGenerator.cs | 165 ++++++++++++++++++ .../Yafc.I18n.Generator.csproj | 22 +++ 4 files changed, 197 insertions(+) create mode 100644 Yafc.I18n.Generator/SourceGenerator.cs create mode 100644 Yafc.I18n.Generator/Yafc.I18n.Generator.csproj diff --git a/.gitignore b/.gitignore index b3e98a60..9f505540 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ ## Custom ignores that were not the part of the conventional file Build/ +*.g.cs # Debug launch configuration Y[Aa][Ff][Cc]/Properties/launchSettings.json diff --git a/FactorioCalc.sln b/FactorioCalc.sln index fa352f30..efdd586a 100644 --- a/FactorioCalc.sln +++ b/FactorioCalc.sln @@ -24,6 +24,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.I18n", "Yafc.I18n\Yafc.I18n.csproj", "{4FEC38A5-A997-48C9-97F5-87BD12119F44}" + ProjectSection(ProjectDependencies) = postProject + {E8A28A02-99C4-41D3-99E3-E6252BD116B7} = {E8A28A02-99C4-41D3-99E3-E6252BD116B7} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yafc.I18n.Generator", "Yafc.I18n.Generator\Yafc.I18n.Generator.csproj", "{E8A28A02-99C4-41D3-99E3-E6252BD116B7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -59,6 +64,10 @@ Global {4FEC38A5-A997-48C9-97F5-87BD12119F44}.Debug|Any CPU.Build.0 = Debug|Any CPU {4FEC38A5-A997-48C9-97F5-87BD12119F44}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FEC38A5-A997-48C9-97F5-87BD12119F44}.Release|Any CPU.Build.0 = Release|Any CPU + {E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8A28A02-99C4-41D3-99E3-E6252BD116B7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Yafc.I18n.Generator/SourceGenerator.cs b/Yafc.I18n.Generator/SourceGenerator.cs new file mode 100644 index 00000000..17e542ec --- /dev/null +++ b/Yafc.I18n.Generator/SourceGenerator.cs @@ -0,0 +1,165 @@ +using System.Text.RegularExpressions; + +namespace Yafc.I18n.Generator; + +internal partial class SourceGenerator { + private static readonly Dictionary localizationKeys = []; + + private static void Main() { + // Find the solution root directory + string rootDirectory = Environment.CurrentDirectory; + while (!Directory.Exists(Path.Combine(rootDirectory, ".git"))) { + rootDirectory = Path.GetDirectoryName(rootDirectory)!; + } + Environment.CurrentDirectory = rootDirectory; + + HashSet keys = []; + HashSet referencedKeys = []; + + using MemoryStream classesMemory = new(), stringsMemory = new(); + using (StreamWriter classes = new(classesMemory, leaveOpen: true), strings = new(stringsMemory, leaveOpen: true)) { + + // Always generate the LocalizableString and LocalizableString0 classes + classes.WriteLine(""" + using System.Diagnostics.CodeAnalysis; + + namespace Yafc.I18n; + + #nullable enable + + /// + /// The base class for YAFC's localizable UI strings. + /// + public abstract class LocalizableString { + private protected readonly string key; + + private protected LocalizableString(string key) => this.key = key; + + /// + /// Localize this string using an arbitrary number of parameters. Insufficient parameters will cause the localization to fail, + /// and excess parameters will be ignored. + /// + /// An array of parameter values. + /// The localized string + public string Localize(params object[] args) => LocalisedStringParser.ParseKey(key, args) ?? "Key not found: " + key; + } + + /// + /// A localizable UI string that needs 0 parameters for localization. + /// These strings will implicitly localize when appropriate. + /// + public sealed class LocalizableString0 : LocalizableString { + internal LocalizableString0(string key) : base(key) { } + + /// + /// Localize this string. + /// + /// The localized string + public string L() => LocalisedStringParser.ParseKey(key, []) ?? "Key not found: " + key; + + /// + /// Implicitly localizes a zero-parameter localizable string. + /// + /// The zero-parameter string to be localized + [return: NotNullIfNotNull(nameof(lString))] + public static implicit operator string?(LocalizableString0? lString) => lString?.L(); + } + """); + + HashSet declaredArities = [0]; + + // Generate the beginning of the LSs class + strings.WriteLine(""" + namespace Yafc.I18n; + + /// + /// A class containing localizable strings for each key defined in the English localization file. This name should be read as + /// LocalizableStrings. It is aggressively abbreviated to help keep lines at a reasonable length. + /// + /// This class is auto-generated. To add new localizable strings, add them to Yafc/Data/locale/en/yafc.cfg + /// and build the solution. + public static class LSs { + """); + + // For each key in locale/en/*.* + foreach (string file in Directory.EnumerateFiles(Path.Combine(rootDirectory, "Yafc/Data/locale/en"))) { + using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + foreach (var (category, key, v) in FactorioLocalization.Read(stream)) { + string value = v; // iteration variables are read-only; make value writable. + int parameterCount = 0; + foreach (Match match in FindParameters().Matches(value)) { + parameterCount = Math.Max(parameterCount, int.Parse(match.Groups[1].Value)); + } + + // If we haven't generated it yet, generate the LocalizableString class + if (declaredArities.Add(parameterCount)) { + classes.WriteLine($$""" + + /// + /// A localizable string that needs {{parameterCount}} parameters for localization. + /// + public sealed class LocalizableString{{parameterCount}} : LocalizableString { + internal LocalizableString{{parameterCount}}(string key) : base(key) { } + + /// + /// Localize this string. + /// + {{string.Join(Environment.NewLine, Enumerable.Range(1, parameterCount).Select(n => $" /// The value to use for parameter __{n}__ when localizing this string."))}} + /// The localized string + public string L({{string.Join(", ", Enumerable.Range(1, parameterCount).Select(n => $"object p{n}"))}}) + => LocalisedStringParser.ParseKey(key, [{{string.Join(", ", Enumerable.Range(1, parameterCount).Select(n => $"p{n}"))}}]) ?? "Key not found: " + key; + } + """); + } + + string pascalCasedKey = string.Join("", key.Split('-').Select(s => char.ToUpperInvariant(s[0]) + s[1..])); + keys.Add(key); + + foreach (Match match in FindReferencedKeys().Matches(value)) { + referencedKeys.Add(match.Groups[1].Value); + } + + if (value.Length > 70) { + value = value[..70] + "..."; + } + value = value.Replace("&", "&").Replace("<", "<"); + + // Generate the read-only PascalCasedKeyName field. + strings.WriteLine($$""" + /// + /// Gets a string that will localize to a value resembling "{{value}}" + /// + """); +#if DEBUG + strings.WriteLine($" public static LocalizableString{parameterCount} {pascalCasedKey} {{ get; }} = new(\"{category}.{key}\");"); +#else + // readonly fields are much smaller than read-only properties, but VS doesn't provide inline reference counts for them. + strings.WriteLine($" public static readonly LocalizableString{parameterCount} {pascalCasedKey} = new(\"{category}.{key}\");"); +#endif + } + } + + foreach (string? undefinedKey in referencedKeys.Except(keys)) { + strings.WriteLine($"#error Found a reference to __YAFC__{undefinedKey}__, which is not defined."); + } + // end of class LLs + strings.WriteLine("}"); + } + + ReplaceIfChanged("Yafc.I18n/LocalizableStringClasses.g.cs", classesMemory); + ReplaceIfChanged("Yafc.I18n/LocalizableStrings.g.cs", stringsMemory); + } + + // Replace the files only if the new content is different than the old content. + private static void ReplaceIfChanged(string filePath, MemoryStream newContent) { + newContent.Position = 0; + if (!File.Exists(filePath) || File.ReadAllText(filePath) != new StreamReader(newContent, leaveOpen: true).ReadToEnd()) { + File.WriteAllBytes(filePath, newContent.ToArray()); + } + } + + [GeneratedRegex("__(\\d+)__")] + private static partial Regex FindParameters(); + [GeneratedRegex("__YAFC__([a-zA-Z0-9_-]+)__")] + private static partial Regex FindReferencedKeys(); +} diff --git a/Yafc.I18n.Generator/Yafc.I18n.Generator.csproj b/Yafc.I18n.Generator/Yafc.I18n.Generator.csproj new file mode 100644 index 00000000..f33527f8 --- /dev/null +++ b/Yafc.I18n.Generator/Yafc.I18n.Generator.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + + From 9c021c50425c9206cee8b1ba47bd0ac71c2b91c9 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:37:42 -0400 Subject: [PATCH 32/54] feat(i18n): Use localization keys instead of hardcoded strings. --- Docs/MoreLanguagesSupport.md | 2 +- Yafc.Model/Analysis/Analysis.cs | 3 +- Yafc.Model/Analysis/CostAnalysis.cs | 30 +- Yafc.Model/Analysis/DependencyNode.cs | 3 +- Yafc.Model/Analysis/Milestones.cs | 16 +- Yafc.Model/Blueprints/BlueprintUtilities.cs | 2 +- Yafc.Model/Data/DataClasses.cs | 48 ++-- Yafc.Model/Data/DataUtils.cs | 25 +- Yafc.Model/Model/AutoPlanner.cs | 5 +- Yafc.Model/Model/ProductionSummary.cs | 9 +- Yafc.Model/Model/ProductionTable.cs | 9 +- Yafc.Model/Model/ProductionTableContent.cs | 17 +- Yafc.Model/Model/Project.cs | 9 +- Yafc.Model/Model/ProjectPage.cs | 3 +- Yafc.Model/Serialization/ErrorCollector.cs | 13 +- .../Serialization/PropertySerializers.cs | 3 +- Yafc.Model/Serialization/SerializationMap.cs | 5 +- Yafc.Model/Serialization/ValueSerializers.cs | 11 +- Yafc.Model/Yafc.Model.csproj | 1 + Yafc.Parser/Data/FactorioDataDeserializer.cs | 36 +-- .../Data/FactorioDataDeserializer_Context.cs | 36 +-- .../Data/FactorioDataDeserializer_Entity.cs | 13 +- ...rioDataDeserializer_RecipeAndTechnology.cs | 3 +- Yafc.Parser/FactorioDataSource.cs | 14 +- Yafc.Parser/LuaContext.cs | 11 +- Yafc.UI/Core/ExceptionScreen.cs | 7 +- Yafc.UI/ImGui/ImGuiUtils.cs | 12 +- Yafc.UI/Yafc.UI.csproj | 4 + Yafc/Program.cs | 10 +- Yafc/Utils/CommandLineParser.cs | 60 +--- Yafc/Widgets/ImmediateWidgets.cs | 28 +- Yafc/Widgets/MainScreenTabBar.cs | 9 +- Yafc/Widgets/ObjectTooltip.cs | 186 +++++++------ Yafc/Windows/AboutScreen.cs | 57 ++-- Yafc/Windows/DependencyExplorer.cs | 67 ++--- Yafc/Windows/ErrorListPanel.cs | 9 +- Yafc/Windows/FilesystemScreen.cs | 3 +- Yafc/Windows/ImageSharePanel.cs | 11 +- Yafc/Windows/MainScreen.PageListSearch.cs | 21 +- Yafc/Windows/MainScreen.cs | 83 +++--- Yafc/Windows/MilestonesEditor.cs | 14 +- Yafc/Windows/MilestonesPanel.cs | 16 +- Yafc/Windows/NeverEnoughItemsPanel.cs | 42 +-- Yafc/Windows/PreferencesScreen.cs | 76 +++--- Yafc/Windows/ProjectPageSettingsPanel.cs | 43 +-- Yafc/Windows/SelectMultiObjectPanel.cs | 5 +- Yafc/Windows/SelectObjectPanel.cs | 3 +- Yafc/Windows/ShoppingListScreen.cs | 63 ++--- Yafc/Windows/WelcomeScreen.cs | 95 +++---- Yafc/Windows/WizardPanel.cs | 7 +- Yafc/Workspace/AutoPlannerView.cs | 15 +- .../ProductionSummaryView.cs | 17 +- .../ModuleCustomizationScreen.cs | 61 +++-- .../ModuleFillerParametersScreen.cs | 54 ++-- .../ModuleTemplateConfiguration.cs | 7 +- .../ProductionLinkSummaryScreen.cs | 41 +-- .../ProductionTableFlatHierarchy.cs | 3 +- .../ProductionTable/ProductionTableView.cs | 256 +++++++++--------- Yafc/Workspace/SummaryView.cs | 13 +- 59 files changed, 855 insertions(+), 870 deletions(-) diff --git a/Docs/MoreLanguagesSupport.md b/Docs/MoreLanguagesSupport.md index 7f3fa704..533ac561 100644 --- a/Docs/MoreLanguagesSupport.md +++ b/Docs/MoreLanguagesSupport.md @@ -1,6 +1,6 @@ # YAFC support for more languages -You can ask Yafc to display non-English names for Factorio objects from the Welcome screen: +You can ask Yafc to display non-English text from the Welcome screen: - On the Welcome screen, click the language name (probably "English") next to "In-game objects language:" - Select your language from the drop-down that appears. - If your language uses non-European glyphs, it may appear at the bottom of the list. diff --git a/Yafc.Model/Analysis/Analysis.cs b/Yafc.Model/Analysis/Analysis.cs index 82b283c0..03c3904d 100644 --- a/Yafc.Model/Analysis/Analysis.cs +++ b/Yafc.Model/Analysis/Analysis.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Yafc.I18n; namespace Yafc.Model; @@ -16,7 +17,7 @@ public abstract class Analysis { public static void ProcessAnalyses(IProgress<(string, string)> progress, Project project, ErrorCollector errors) { foreach (var analysis in analyses) { - progress.Report(("Running analysis algorithms", analysis.GetType().Name)); + progress.Report((LSs.ProgressRunningAnalysis, analysis.GetType().Name)); analysis.Compute(project, errors); } } diff --git a/Yafc.Model/Analysis/CostAnalysis.cs b/Yafc.Model/Analysis/CostAnalysis.cs index a6f4821a..b5e3cb0a 100644 --- a/Yafc.Model/Analysis/CostAnalysis.cs +++ b/Yafc.Model/Analysis/CostAnalysis.cs @@ -5,6 +5,7 @@ using System.Text; using Google.OrTools.LinearSolver; using Serilog; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -51,14 +52,14 @@ public override void Compute(Project project, ErrorCollector warnings) { Dictionary sciencePackUsage = []; if (!onlyCurrentMilestones && project.preferences.targetTechnology != null) { - itemAmountPrefix = "Estimated amount for " + project.preferences.targetTechnology.locName + ": "; + itemAmountPrefix = LSs.CostAnalysisEstimatedAmountFor.L(project.preferences.targetTechnology.locName); foreach (var spUsage in TechnologyScienceAnalysis.Instance.allSciencePacks[project.preferences.targetTechnology]) { sciencePackUsage[spUsage.goods] = spUsage.amount; } } else { - itemAmountPrefix = "Estimated amount for all researches: "; + itemAmountPrefix = LSs.CostAnalysisEstimatedAmount; foreach (Technology technology in Database.technologies.all.ExceptExcluded(this)) { if (technology.IsAccessible() && technology.ingredients is not null) { @@ -352,7 +353,7 @@ public override void Compute(Project project, ErrorCollector warnings) { } else { if (!onlyCurrentMilestones) { - warnings.Error("Cost analysis was unable to process this modpack. This may indicate a bug in Yafc.", ErrorSeverity.AnalysisWarning); + warnings.Error(LSs.CostAnalysisFailed, ErrorSeverity.AnalysisWarning); } } @@ -362,45 +363,40 @@ public override void Compute(Project project, ErrorCollector warnings) { workspaceSolver.Dispose(); } - private static readonly StringBuilder sb = new StringBuilder(); public static string GetDisplayCost(FactorioObject goods) { float cost = goods.Cost(); float costNow = goods.Cost(true); if (float.IsPositiveInfinity(cost)) { - return "YAFC analysis: Unable to find a way to fully automate this"; + return LSs.AnalysisNotAutomatable; } - _ = sb.Clear(); - float compareCost = cost; float compareCostNow = costNow; - string costPrefix; + string finalCost; if (goods is Fluid) { compareCost = cost * 50; compareCostNow = costNow * 50; - costPrefix = "YAFC cost per 50 units of fluid:"; + finalCost = LSs.CostAnalysisFluidCost.L(DataUtils.FormatAmount(compareCost, UnitOfMeasure.None)); } else if (goods is Item) { - costPrefix = "YAFC cost per item:"; + finalCost = LSs.CostAnalysisItemCost.L(DataUtils.FormatAmount(compareCost, UnitOfMeasure.None)); } else if (goods is Special special && special.isPower) { - costPrefix = "YAFC cost per 1 MW:"; + finalCost = LSs.CostAnalysisEnergyCost.L(DataUtils.FormatAmount(compareCost, UnitOfMeasure.None)); } else if (goods is Recipe) { - costPrefix = "YAFC cost per recipe:"; + finalCost = LSs.CostAnalysisRecipeCost.L(DataUtils.FormatAmount(compareCost, UnitOfMeasure.None)); } else { - costPrefix = "YAFC cost:"; + finalCost = LSs.CostAnalysisGenericCost.L(DataUtils.FormatAmount(compareCost, UnitOfMeasure.None)); } - _ = sb.Append(costPrefix).Append(" ¥").Append(DataUtils.FormatAmount(compareCost, UnitOfMeasure.None)); - if (compareCostNow > compareCost && !float.IsPositiveInfinity(compareCostNow)) { - _ = sb.Append(" (Currently ¥").Append(DataUtils.FormatAmount(compareCostNow, UnitOfMeasure.None)).Append(')'); + return LSs.CostAnalysisWithCurrentCost.L(finalCost, DataUtils.FormatAmount(compareCostNow, UnitOfMeasure.None)); } - return sb.ToString(); + return finalCost; } public static float GetBuildingHours(Recipe recipe, float flow) => recipe.time * flow * (1000f / 3600f); diff --git a/Yafc.Model/Analysis/DependencyNode.cs b/Yafc.Model/Analysis/DependencyNode.cs index b6ab7203..2719cde3 100644 --- a/Yafc.Model/Analysis/DependencyNode.cs +++ b/Yafc.Model/Analysis/DependencyNode.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Numerics; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -224,7 +225,7 @@ public override void Draw(ImGui gui, Action, Fl foreach (var dependency in dependencies) { if (!isFirst) { using (gui.EnterGroup(new(1, .25f))) { - gui.BuildText("-- OR --", Font.productionTableHeader); + gui.BuildText(LSs.DependencyOrBar, Font.productionTableHeader); } gui.DrawRectangle(gui.lastRect - offset, SchemeColor.GreyAlt); } diff --git a/Yafc.Model/Analysis/Milestones.cs b/Yafc.Model/Analysis/Milestones.cs index a884d562..ad0fddf4 100644 --- a/Yafc.Model/Analysis/Milestones.cs +++ b/Yafc.Model/Analysis/Milestones.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Serilog; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -188,16 +189,13 @@ public void ComputeWithParameters(Project project, ErrorCollector warnings, Fact bool hasAutomatableRocketLaunch = result[Database.objectsByTypeName["Special.launch"]] != 0; List milestonesNotReachable = [.. milestones.Except(sortedMilestones)]; if (accessibleObjects < Database.objects.count / 2) { - warnings.Error("More than 50% of all in-game objects appear to be inaccessible in this project with your current mod list. This can have a variety of reasons like objects " + - "being accessible via scripts," + MaybeBug + MilestoneAnalysisIsImportant + UseDependencyExplorer, ErrorSeverity.AnalysisWarning); + warnings.Error(LSs.MilestoneAnalysisMostInaccessible, ErrorSeverity.AnalysisWarning); } else if (!hasAutomatableRocketLaunch) { - warnings.Error("Rocket launch appear to be inaccessible. This means that rocket may not be launched in this mod pack, or it requires mod script to spawn or unlock some items," + - MaybeBug + MilestoneAnalysisIsImportant + UseDependencyExplorer, ErrorSeverity.AnalysisWarning); + warnings.Error(LSs.MilestoneAnalysisNoRocketLaunch, ErrorSeverity.AnalysisWarning); } else if (milestonesNotReachable.Count > 0) { - warnings.Error("There are some milestones that are not accessible: " + string.Join(", ", milestonesNotReachable.Select(x => x.locName)) + - ". You may remove these from milestone list," + MaybeBug + MilestoneAnalysisIsImportant + UseDependencyExplorer, ErrorSeverity.AnalysisWarning); + warnings.Error(LSs.MilestoneAnalysisInaccessibleMilestones.L(string.Join(LSs.ListSeparator, milestonesNotReachable.Select(x => x.locName))), ErrorSeverity.AnalysisWarning); } logger.Information("Milestones calculation finished in {ElapsedTime}ms.", time.ElapsedMilliseconds); @@ -259,10 +257,4 @@ private static bool[] WalkAccessibilityGraph(Project project, HashSet item, int amount)> goods, EntityContainer chest, bool copyToClipboard = true) { if (chest.logisticSlotsCount <= 0) { - throw new NotSupportedException("Chest does not have logistic slots"); + throw new ArgumentException("Chest does not have logistic slots"); } int combinatorCount = ((goods.Count - 1) / chest.logisticSlotsCount) + 1; diff --git a/Yafc.Model/Data/DataClasses.cs b/Yafc.Model/Data/DataClasses.cs index 867b210f..09b1ca38 100644 --- a/Yafc.Model/Data/DataClasses.cs +++ b/Yafc.Model/Data/DataClasses.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; +using Yafc.I18n; using Yafc.UI; [assembly: InternalsVisibleTo("Yafc.Parser")] @@ -47,14 +48,14 @@ public abstract class FactorioObject : IFactorioObjectWrapper, IComparable locName; - public void FallbackLocalization(FactorioObject? other, string description) { + public void FallbackLocalization(FactorioObject? other, LocalizableString1 description) { if (locName == null) { if (other == null) { locName = name; } else { locName = other.locName; - locDescr = description + " " + locName; + locDescr = description.L(locName); } } @@ -210,6 +211,7 @@ protected override List GetDependenciesHelper() { public class Mechanics : Recipe { internal FactorioObject source { get; set; } = null!; // null-forgiving: Set by CreateSpecialRecipe internal override FactorioObjectSortOrder sortingOrder => FactorioObjectSortOrder.Mechanics; + internal LocalizableString localizationKey = null!; // null-forgiving: Set by CreateSpecialRecipe public override string type => "Mechanics"; } @@ -230,11 +232,11 @@ string IFactorioObjectWrapper.text { get { string text = goods.locName; if (amount != 1f) { - text = amount + "x " + text; + text = LSs.IngredientAmount.L(amount, goods.locName); } if (!temperature.IsAny()) { - text += " (" + temperature + ")"; + text = LSs.IngredientAmountWithTemperature.L(amount, goods.locName, temperature); } return text; @@ -313,27 +315,35 @@ public Product(Goods goods, float min, float max, float probability) { string IFactorioObjectWrapper.text { get { - string text = goods.locName; - - if (amountMin != 1f || amountMax != 1f) { - text = DataUtils.FormatAmount(amountMax, UnitOfMeasure.None) + "x " + text; + string text; - if (amountMin != amountMax) { - text = DataUtils.FormatAmount(amountMin, UnitOfMeasure.None) + "-" + text; + if (probability == 1) { + if (amountMin == amountMax) { + text = LSs.ProductAmount.L(DataUtils.FormatAmount(amountMax, UnitOfMeasure.None), goods.locName); + } + else { + text = LSs.ProductAmountRange.L(DataUtils.FormatAmount(amountMin, UnitOfMeasure.None), DataUtils.FormatAmount(amountMax, UnitOfMeasure.None), goods.locName); } } - if (probability != 1f) { - text = DataUtils.FormatAmount(probability, UnitOfMeasure.Percent) + " " + text; - } - else if (amountMin == 1 && amountMax == 1) { - text = "1x " + text; + else { + if (amountMin == amountMax) { + if (amountMin == 1) { + text = LSs.ProductProbability.L(DataUtils.FormatAmount(probability, UnitOfMeasure.Percent), goods.locName); + } + else { + text = LSs.ProductProbabilityAmount.L(DataUtils.FormatAmount(probability, UnitOfMeasure.Percent), DataUtils.FormatAmount(amountMax, UnitOfMeasure.None), goods.locName); + } + } + else { + text = LSs.ProductProbabilityAmountRange.L(DataUtils.FormatAmount(probability, UnitOfMeasure.Percent), DataUtils.FormatAmount(amountMin, UnitOfMeasure.None), DataUtils.FormatAmount(amountMax, UnitOfMeasure.None), goods.locName); + } } if (percentSpoiled == 0) { - text += ", always fresh"; + text = LSs.ProductAlwaysFresh.L(text); } else if (percentSpoiled != null) { - text += ", " + DataUtils.FormatAmount(percentSpoiled.Value, UnitOfMeasure.Percent) + " spoiled"; + text = LSs.ProductFixedSpoilage.L(text, DataUtils.FormatAmount(percentSpoiled.Value, UnitOfMeasure.Percent)); } return text; @@ -1040,10 +1050,10 @@ public TemperatureRange(int single) : this(single, single) { } public override readonly string ToString() { if (min == max) { - return min + "°"; + return LSs.Temperature.L(min); } - return min + "°-" + max + "°"; + return LSs.TemperatureRange.L(min, max); } public readonly bool Contains(int value) => min <= value && max >= value; diff --git a/Yafc.Model/Data/DataUtils.cs b/Yafc.Model/Data/DataUtils.cs index bc190324..a8019f6c 100644 --- a/Yafc.Model/Data/DataUtils.cs +++ b/Yafc.Model/Data/DataUtils.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using Google.OrTools.LinearSolver; using Serilog; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -160,10 +161,10 @@ public int Compare(T? x, T? y) { T? element = null; if (list.Any(t => t.IsAccessible())) { - recipeHint = "Hint: Complete milestones to enable ctrl+click"; + recipeHint = LSs.CtrlClickHintCompleteMilestones; } else { - recipeHint = "Hint: Mark a recipe as accessible to enable ctrl+click"; + recipeHint = LSs.CtrlClickHintMarkAccessible; } foreach (T elem in list) { @@ -175,11 +176,11 @@ public int Compare(T? x, T? y) { if (userFavorites.Contains(elem)) { if (!acceptOnlyFavorites || element == null) { element = elem; - recipeHint = "Hint: ctrl+click to add your favorited recipe"; + recipeHint = LSs.CtrlClickHintWillAddFavorite; acceptOnlyFavorites = true; } else { - recipeHint = "Hint: Cannot ctrl+click with multiple favorited recipes"; + recipeHint = LSs.CtrlClickHintMultipleFavorites; return null; } @@ -187,11 +188,11 @@ public int Compare(T? x, T? y) { else if (!acceptOnlyFavorites) { if (element == null) { element = elem; - recipeHint = excludeSpecial ? "Hint: ctrl+click to add the accessible normal recipe" : "Hint: ctrl+click to add the accessible recipe"; + recipeHint = excludeSpecial ? LSs.CtrlClickHintWillAddNormal : LSs.CtrlClickHintWillAddSpecial; } else { element = null; - recipeHint = "Hint: Set a favorite recipe to add it with ctrl+click"; + recipeHint = LSs.CtrlClickHintSetFavorite; acceptOnlyFavorites = true; } } @@ -542,26 +543,26 @@ public static string FormatTime(float time) { _ = amountBuilder.Clear(); if (time < 10f) { - return $"{time:#.#} seconds"; + return LSs.FormatTimeInSeconds.L(time.ToString("#.#")); } if (time < 60f) { - return $"{time:#} seconds"; + return LSs.FormatTimeInSeconds.L(time.ToString("#")); } if (time < 600f) { - return $"{time / 60f:#.#} minutes"; + return LSs.FormatTimeInMinutes.L((time / 60f).ToString("#.#")); } if (time < 3600f) { - return $"{time / 60f:#} minutes"; + return LSs.FormatTimeInMinutes.L((time / 60f).ToString("#")); } if (time < 36000f) { - return $"{time / 3600f:#.#} hours"; + return LSs.FormatTimeInHours.L((time / 3600f).ToString("#.#")); } - return $"{time / 3600f:#} hours"; + return LSs.FormatTimeInHours.L((time / 3600f).ToString("#")); } public static string FormatAmount(float amount, UnitOfMeasure unit, bool precise = false) { diff --git a/Yafc.Model/Model/AutoPlanner.cs b/Yafc.Model/Model/AutoPlanner.cs index c52cb217..528cea4b 100644 --- a/Yafc.Model/Model/AutoPlanner.cs +++ b/Yafc.Model/Model/AutoPlanner.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Google.OrTools.LinearSolver; using Serilog; +using Yafc.I18n; using Yafc.UI; #nullable disable warnings // Disabling nullable in legacy code. @@ -15,7 +16,7 @@ public class AutoPlannerGoal { private Goods _item; public Goods item { get => _item; - set => _item = value ?? throw new ArgumentNullException(nameof(value), "Auto planner goal no longer exist"); + set => _item = value ?? throw new ArgumentNullException(nameof(value), LSs.AutoPlannerMissingGoal); } public float amount { get; set; } } @@ -118,7 +119,7 @@ public override async Task Solve(ProjectPage page) { logger.Information(bestFlowSolver.ExportModelAsLpFormat(false)); this.tiers = null; - return "Model has no solution"; + return LSs.AutoPlannerNoSolution; } Graph graph = new Graph(); diff --git a/Yafc.Model/Model/ProductionSummary.cs b/Yafc.Model/Model/ProductionSummary.cs index 9fa1b833..97d4cdd6 100644 --- a/Yafc.Model/Model/ProductionSummary.cs +++ b/Yafc.Model/Model/ProductionSummary.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -36,7 +37,7 @@ public class ProductionSummaryEntry(ProductionSummaryGroup owner) : ModelObject< protected internal override void AfterDeserialize() { // Must be either page reference, or subgroup, not both if (subgroup == null && page == null) { - throw new NotSupportedException("Referenced page does not exist"); + throw new NotSupportedException(LSs.LoadErrorReferencedPageNotFound); } if (subgroup != null && page != null) { @@ -70,10 +71,10 @@ public Icon icon { public string name { get { if (page != null) { - return page.page?.name ?? "Page missing"; + return page.page?.name ?? LSs.LegacySummaryPageMissing; } - return "Broken entry"; + return LSs.LegacySummaryBrokenEntry; } } @@ -155,7 +156,7 @@ 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 IObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), LSs.LoadErrorObjectDoesNotExist); } public class ProductionSummary : ProjectPageContents, IComparer<(IObjectWithQuality goods, float amount)> { diff --git a/Yafc.Model/Model/ProductionTable.cs b/Yafc.Model/Model/ProductionTable.cs index 6d0ab6c2..1fe24015 100644 --- a/Yafc.Model/Model/ProductionTable.cs +++ b/Yafc.Model/Model/ProductionTable.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Google.OrTools.LinearSolver; using Serilog; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -607,14 +608,14 @@ private static void AddLinkCoefficient(Constraint cst, Variable var, IProduction } else { if (result == Solver.ResultStatus.INFEASIBLE) { - return "YAFC failed to solve the model and to find deadlock loops. As a result, the model was not updated."; + return LSs.ProductionTableNoSolutionAndNoDeadlocks; } if (result == Solver.ResultStatus.ABNORMAL) { - return "This model has numerical errors (probably too small or too large numbers) and cannot be solved"; + return LSs.ProductionTableNumericalErrors; } - return "Unaccounted error: MODEL_" + result; + return LSs.ProductionTableUnexpectedError.L(result); } } @@ -643,7 +644,7 @@ private static void AddLinkCoefficient(Constraint cst, Variable var, IProduction CalculateFlow(null); - return builtCountExceeded ? "This model requires more buildings than are currently built" : null; + return builtCountExceeded ? LSs.ProductionTableRequiresMoreBuildings : null; } /// diff --git a/Yafc.Model/Model/ProductionTableContent.cs b/Yafc.Model/Model/ProductionTableContent.cs index 63bd45b1..12f7d924 100644 --- a/Yafc.Model/Model/ProductionTableContent.cs +++ b/Yafc.Model/Model/ProductionTableContent.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -630,7 +631,7 @@ public void ChangeVariant(T was, T now) where T : FactorioObject { public bool visible { get; internal set; } = true; public RecipeRow(ProductionTable owner, IObjectWithQuality recipe) : base(owner) { - this.recipe = recipe ?? throw new ArgumentNullException(nameof(recipe), "Recipe does not exist"); + this.recipe = recipe ?? throw new ArgumentNullException(nameof(recipe), LSs.LoadErrorRecipeDoesNotExist); links = new RecipeLinks { ingredients = new ProductionLink[recipe.target.ingredients.Length], @@ -841,7 +842,7 @@ public enum Flags { HasProductionAndConsumption = HasProduction | HasConsumption, } - public IObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), "Linked product does not exist"); + public IObjectWithQuality goods { get; } = goods ?? throw new ArgumentNullException(nameof(goods), LSs.LoadErrorLinkedProductDoesNotExist); public float amount { get; set; } public LinkAlgorithm algorithm { get; set; } public UnitOfMeasure flowUnitOfMeasure => goods.target.flowUnitOfMeasure; @@ -868,27 +869,27 @@ public enum Flags { public IEnumerable LinkWarnings { get { if (!flags.HasFlags(Flags.HasProduction)) { - yield return "This link has no production (Link ignored)"; + yield return LSs.LinkWarningNoProduction; } if (!flags.HasFlags(Flags.HasConsumption)) { - yield return "This link has no consumption (Link ignored)"; + yield return LSs.LinkWarningNoConsumption; } if (flags.HasFlags(Flags.ChildNotMatched)) { - yield return "Nested table link has unmatched production/consumption. These unmatched products are not captured by this link."; + yield return LSs.LinkWarningUnmatchedNestedLink; } if (!flags.HasFlags(Flags.HasProductionAndConsumption) && owner.owner is RecipeRow recipeRow && recipeRow.FindLink(goods, out _)) { - yield return "Nested tables have their own set of links that DON'T connect to parent links. To connect this product to the outside, remove this link."; + yield return LSs.LinkMessageRemoveToLinkWithParent; } if (flags.HasFlags(Flags.LinkRecursiveNotMatched)) { if (notMatchedFlow <= 0f) { - yield return "YAFC was unable to satisfy this link (Negative feedback loop). This doesn't mean that this link is the problem, but it is part of the loop."; + yield return LSs.LinkWarningNegativeFeedback; } else { - yield return "YAFC was unable to satisfy this link (Overproduction). You can allow overproduction for this link to solve the error."; + yield return LSs.LinkWarningNeedsOverproduction; } } } diff --git a/Yafc.Model/Model/Project.cs b/Yafc.Model/Model/Project.cs index a4109507..deb95f84 100644 --- a/Yafc.Model/Model/Project.cs +++ b/Yafc.Model/Model/Project.cs @@ -5,6 +5,7 @@ using System.Runtime.Serialization; using System.Text.Json; using System.Text.RegularExpressions; +using Yafc.I18n; namespace Yafc.Model; @@ -87,7 +88,7 @@ public static Project ReadFromFile(string path, ErrorCollector collector, bool u return read(path, collector, useMostRecent); } catch when (useMostRecent) { - collector.Error("Fatal error reading the latest autosave. Loading the base file instead.", ErrorSeverity.Important); + collector.Error(LSs.ErrorLoadingAutosave, ErrorSeverity.Important); return read(path, collector, false); } @@ -144,11 +145,11 @@ public static Project Read(byte[] bytes, ErrorCollector collector) { project = SerializationMap.DeserializeFromJson(null, ref reader, context); if (!reader.IsFinalBlock) { - collector.Error("Json was not consumed to the end!", ErrorSeverity.MajorDataLoss); + collector.Error(LSs.LoadErrorDidNotReadAllData, ErrorSeverity.MajorDataLoss); } if (project == null) { - throw new SerializationException("Unable to load project file"); + throw new SerializationException(LSs.LoadErrorUnableToLoadFile); } project.justCreated = false; @@ -156,7 +157,7 @@ public static Project Read(byte[] bytes, ErrorCollector collector) { if (version != currentYafcVersion) { if (version > currentYafcVersion) { - collector.Error("This file was created with future YAFC version. This may lose data.", ErrorSeverity.Important); + collector.Error(LSs.LoadWarningNewerVersion, ErrorSeverity.Important); } project.yafcVersion = currentYafcVersion.ToString(); diff --git a/Yafc.Model/Model/ProjectPage.cs b/Yafc.Model/Model/ProjectPage.cs index 180b48a2..9f91467b 100644 --- a/Yafc.Model/Model/ProjectPage.cs +++ b/Yafc.Model/Model/ProjectPage.cs @@ -1,12 +1,13 @@ using System; using System.Threading.Tasks; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; public class ProjectPage : ModelObject { public FactorioObject? icon { get; set; } - public string name { get; set; } = "New page"; + public string name { get; set; } = LSs.DefaultNewPageName; public Guid guid { get; private set; } public Type contentType { get; } public ProjectPageContents content { get; } diff --git a/Yafc.Model/Serialization/ErrorCollector.cs b/Yafc.Model/Serialization/ErrorCollector.cs index b863e3a9..83fadf66 100644 --- a/Yafc.Model/Serialization/ErrorCollector.cs +++ b/Yafc.Model/Serialization/ErrorCollector.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.Json; using Serilog; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -34,7 +35,7 @@ public void Error(string message, ErrorSeverity severity) { public (string error, ErrorSeverity severity)[] GetArrErrors() => [.. allErrors.OrderByDescending(x => x.Key.severity).ThenByDescending(x => x.Value) - .Select(x => (x.Value == 1 ? x.Key.message : x.Key.message + " (x" + x.Value + ")", x.Key.severity))]; + .Select(x => (x.Value == 1 ? x.Key.message : LSs.RepeatedError.L(x.Key.message, x.Value), x.Key.severity))]; public void Exception(Exception exception, string message, ErrorSeverity errorSeverity) { while (exception.InnerException != null) { @@ -46,14 +47,8 @@ public void Exception(Exception exception, string message, ErrorSeverity errorSe if (exception is JsonException) { s += "unexpected or invalid json"; } - else if (exception is ArgumentNullException argnull) { - s += argnull.Message; - } - else if (exception is NotSupportedException notSupportedException) { - s += notSupportedException.Message; - } - else if (exception is InvalidOperationException unexpectedNull) { - s += unexpectedNull.Message; + else if (exception is ArgumentNullException or NotSupportedException or InvalidOperationException) { + s += exception.Message; } else { s += exception.GetType().Name; diff --git a/Yafc.Model/Serialization/PropertySerializers.cs b/Yafc.Model/Serialization/PropertySerializers.cs index 27b47f9c..4980ccdc 100644 --- a/Yafc.Model/Serialization/PropertySerializers.cs +++ b/Yafc.Model/Serialization/PropertySerializers.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; +using Yafc.I18n; namespace Yafc.Model; @@ -151,7 +152,7 @@ public override void DeserializeFromJson(TOwner owner, ref Utf8JsonReader reader var instance = getter(owner); if (instance == null) { - context.Error("Project contained an unexpected object", ErrorSeverity.MinorDataLoss); + context.Error(LSs.LoadWarningUnexpectedObject, ErrorSeverity.MinorDataLoss); reader.Skip(); } else if (instance.GetType() == typeof(TPropertyType)) { diff --git a/Yafc.Model/Serialization/SerializationMap.cs b/Yafc.Model/Serialization/SerializationMap.cs index e9f08883..9a50b7e3 100644 --- a/Yafc.Model/Serialization/SerializationMap.cs +++ b/Yafc.Model/Serialization/SerializationMap.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Text.Json; using Serilog; +using Yafc.I18n; using Yafc.UI; namespace Yafc.Model; @@ -391,7 +392,7 @@ public bool Deserialize(ref Utf8JsonReader reader, DeserializationContext contex return obj; } catch (Exception ex) { - context.Exception(ex, "Unable to deserialize " + typeof(T).Name, ErrorSeverity.MajorDataLoss); + context.Exception(ex, LSs.LoadErrorUnableToDeserializeUntranslated.L(typeof(T).Name), ErrorSeverity.MajorDataLoss); if (reader.TokenType == JsonTokenType.StartObject && reader.CurrentDepth == depth) { _ = reader.Read(); @@ -433,7 +434,7 @@ public static void PopulateFromJson(T obj, ref Utf8JsonReader reader, Deserializ property.DeserializeFromJson(obj, ref reader, allObjects); } catch (InvalidOperationException ex) { - allObjects.Exception(ex, "Encountered an unexpected value when reading the project file", ErrorSeverity.MajorDataLoss); + allObjects.Exception(ex, LSs.LoadErrorEncounteredUnexpectedValue, ErrorSeverity.MajorDataLoss); } } _ = reader.Read(); diff --git a/Yafc.Model/Serialization/ValueSerializers.cs b/Yafc.Model/Serialization/ValueSerializers.cs index 0db578e4..d2a1609b 100644 --- a/Yafc.Model/Serialization/ValueSerializers.cs +++ b/Yafc.Model/Serialization/ValueSerializers.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.RegularExpressions; +using Yafc.I18n; namespace Yafc.Model; @@ -315,7 +316,7 @@ internal class TypeSerializer : ValueSerializer { } Type? type = Type.GetType(s); if (type == null) { - context.Error("Type " + s + " does not exist. Possible plugin version change", ErrorSeverity.MinorDataLoss); + context.Error(LSs.LoadErrorUntranslatedTypeDoesNotExist.L(s), ErrorSeverity.MinorDataLoss); } return type; @@ -387,11 +388,11 @@ internal class FactorioObjectSerializer : ValueSerializer where T : Factor var substitute = Database.FindClosestVariant(s); if (substitute is T t) { - context.Error("Fluid " + t.locName + " doesn't have correct temperature information. May require adjusting its temperature.", ErrorSeverity.MinorDataLoss); + context.Error(LSs.LoadErrorFluidHasIncorrectTemperature.L(t.locName), ErrorSeverity.MinorDataLoss); return t; } - context.Error("Factorio object '" + s + "' no longer exist. Check mods configuration.", ErrorSeverity.MinorDataLoss); + context.Error(LSs.LoadErrorUntranslatedFactorioObjectNotFound.L(s), ErrorSeverity.MinorDataLoss); } return obj as T; } @@ -430,10 +431,10 @@ internal sealed partial class QualityObjectSerializer : ValueSerializer + diff --git a/Yafc.Parser/Data/FactorioDataDeserializer.cs b/Yafc.Parser/Data/FactorioDataDeserializer.cs index 7ba7e431..d422d5e1 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer.cs @@ -76,7 +76,7 @@ private void UpdateSplitFluids() { } fluid.variants.Sort(DataUtils.FluidTemperatureComparer); - fluidVariants[fluid.type + "." + fluid.name] = fluid.variants; + fluidVariants[fluid.typeDotName] = fluid.variants; foreach (var variant in fluid.variants) { AddTemperatureToFluidIcon(variant); @@ -114,7 +114,7 @@ private static void AddTemperatureToFluidIcon(Fluid fluid) { public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes, bool netProduction, IProgress<(string, string)> progress, ErrorCollector errorCollector, bool renderIcons, bool useLatestSave) { - progress.Report(("Loading", "Loading items")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingItems)); raw = (LuaTable?)data["raw"] ?? throw new ArgumentException("Could not load data.raw from data argument", nameof(data)); LuaTable itemPrototypes = (LuaTable?)prototypes?["item"] ?? throw new ArgumentException("Could not load prototypes.item from data argument", nameof(prototypes)); @@ -127,25 +127,25 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes, } allModules.AddRange(allObjects.OfType()); - progress.Report(("Loading", "Loading tiles")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingTiles)); DeserializePrototypes(raw, "tile", DeserializeTile, progress, errorCollector); - progress.Report(("Loading", "Loading fluids")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingFluids)); DeserializePrototypes(raw, "fluid", DeserializeFluid, progress, errorCollector); - progress.Report(("Loading", "Loading recipes")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingRecipes)); DeserializePrototypes(raw, "recipe", DeserializeRecipe, progress, errorCollector); - progress.Report(("Loading", "Loading locations")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingLocations)); DeserializePrototypes(raw, "planet", DeserializeLocation, progress, errorCollector); DeserializePrototypes(raw, "space-location", DeserializeLocation, progress, errorCollector); rootAccessible.Add(GetObject("nauvis")); - progress.Report(("Loading", "Loading technologies")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingTechnologies)); DeserializePrototypes(raw, "technology", DeserializeTechnology, progress, errorCollector); - progress.Report(("Loading", "Loading qualities")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingQualities)); DeserializePrototypes(raw, "quality", DeserializeQuality, progress, errorCollector); Quality.Normal = GetObject("normal"); rootAccessible.Add(Quality.Normal); DeserializePrototypes(raw, "asteroid-chunk", DeserializeAsteroidChunk, progress, errorCollector); - progress.Report(("Loading", "Loading entities")); + progress.Report((LSs.ProgressLoading, LSs.ProgressLoadingEntities)); LuaTable entityPrototypes = (LuaTable?)prototypes["entity"] ?? throw new ArgumentException("Could not load prototypes.entity from data argument", nameof(prototypes)); foreach (object prototypeName in entityPrototypes.ObjectElements.Keys) { DeserializePrototypes(raw, (string)prototypeName, DeserializeEntity, progress, errorCollector); @@ -154,7 +154,7 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes, ParseCaptureEffects(); ParseModYafcHandles(data["script_enabled"] as LuaTable); - progress.Report(("Post-processing", "Computing maps")); + progress.Report((LSs.ProgressPostprocessing, LSs.ProgressComputingMaps)); // Deterministically sort all objects allObjects.Sort((a, b) => a.sortingOrder == b.sortingOrder ? string.Compare(a.typeDotName, b.typeDotName, StringComparison.Ordinal) : a.sortingOrder - b.sortingOrder); @@ -172,13 +172,13 @@ public Project LoadData(string projectPath, LuaTable data, LuaTable prototypes, CalculateItemWeights(); ObjectWithQuality.LoadCache(allObjects); ExportBuiltData(); - progress.Report(("Post-processing", "Calculating dependencies")); + progress.Report((LSs.ProgressPostprocessing, LSs.ProgressCalculatingDependencies)); Dependencies.Calculate(); TechnologyLoopsFinder.FindTechnologyLoops(); - progress.Report(("Post-processing", "Creating project")); + progress.Report((LSs.ProgressPostprocessing, LSs.ProgressCreatingProject)); Project project = Project.ReadFromFile(projectPath, errorCollector, useLatestSave); Analysis.ProcessAnalyses(progress, project, errorCollector); - progress.Report(("Rendering icons", "")); + progress.Report((LSs.ProgressRenderingIcons, "")); iconRenderedProgress = progress; iconRenderTask.Wait(); @@ -206,7 +206,7 @@ private void RenderIcons() { foreach (var o in allObjects) { if (++rendered % 100 == 0) { - iconRenderedProgress?.Report(("Rendering icons", $"{rendered}/{allObjects.Count}")); + iconRenderedProgress?.Report((LSs.ProgressRenderingIcons, LSs.ProgressRenderingXOfY.L(rendered, allObjects.Count))); } if (o.iconSpec != null && o.iconSpec.Length > 0) { @@ -319,7 +319,7 @@ private static void DeserializePrototypes(LuaTable data, string type, Action progress, ErrorCollector errorCollector) { object? table = data[type]; - progress.Report(("Building objects", type)); + progress.Report((LSs.ProgressBuildingObjects, type)); if (table is not LuaTable luaTable) { return; @@ -458,7 +458,7 @@ void readTrigger(LuaTable table) { } if (GetRef(table, "spoil_result", out Item? spoiled)) { - var recipe = CreateSpecialRecipe(item, SpecialNames.SpoilRecipe, "spoiling"); + var recipe = CreateSpecialRecipe(item, SpecialNames.SpoilRecipe, LSs.SpecialRecipeSpoiling); recipe.ingredients = [new Ingredient(item, 1)]; recipe.products = [new Product(spoiled, 1)]; recipe.time = table.Get("spoil_ticks", 0) / 60f; @@ -594,7 +594,7 @@ private void CalculateItemWeights() { /// The result of launching , if known. Otherwise , to preserve /// the existing launch products of a preexisting recipe, or set no products for a new recipe. private void EnsureLaunchRecipe(Item item, Product[]? launchProducts) { - Recipe recipe = CreateSpecialRecipe(item, SpecialNames.RocketLaunch, "launched"); + Recipe recipe = CreateSpecialRecipe(item, SpecialNames.RocketLaunch, LSs.SpecialRecipeLaunched); recipe.ingredients = [ // When this is called, we don't know the item weight or the rocket capacity. @@ -630,7 +630,7 @@ private void DeserializeTile(LuaTable table, ErrorCollector _) { tile.Fluid = pumpingFluid; string recipeCategory = SpecialNames.PumpingRecipe + "tile"; - Recipe recipe = CreateSpecialRecipe(pumpingFluid, recipeCategory, "pumping"); + Recipe recipe = CreateSpecialRecipe(pumpingFluid, recipeCategory, LSs.SpecialRecipePumping); if (recipe.products == null) { recipe.products = [new Product(pumpingFluid, 1200f)]; // set to Factorio default pump amounts - looks nice in tooltip diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs index 53c78c32..7ed8bded 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Context.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Yafc.I18n; using Yafc.Model; namespace Yafc.Parser; @@ -72,31 +73,31 @@ Item createSpecialItem(string name, string locName, string locDescr, string icon return obj; } - electricity = createSpecialObject(true, SpecialNames.Electricity, "Electricity", "This is an object that represents electric energy", + electricity = createSpecialObject(true, SpecialNames.Electricity, LSs.SpecialObjectElectricity, LSs.SpecialObjectElectricityDescription, "__core__/graphics/icons/alerts/electricity-icon-unplugged.png", "signal-E"); - heat = createSpecialObject(true, SpecialNames.Heat, "Heat", "This is an object that represents heat energy", "__core__/graphics/arrows/heat-exchange-indication.png", "signal-H"); + heat = createSpecialObject(true, SpecialNames.Heat, LSs.SpecialObjectHeat, LSs.SpecialObjectHeatDescription, "__core__/graphics/arrows/heat-exchange-indication.png", "signal-H"); - voidEnergy = createSpecialObject(true, SpecialNames.Void, "Void", "This is an object that represents infinite energy", "__core__/graphics/icons/mip/infinity.png", "signal-V"); + voidEnergy = createSpecialObject(true, SpecialNames.Void, LSs.SpecialObjectVoid, LSs.SpecialObjectVoidDescription, "__core__/graphics/icons/mip/infinity.png", "signal-V"); voidEnergy.isVoid = true; voidEnergy.isLinkable = false; voidEnergy.showInExplorers = false; rootAccessible.Add(voidEnergy); - rocketLaunch = createSpecialObject(false, SpecialNames.RocketLaunch, "Rocket launch slot", - "This is a slot in a rocket ready to be launched", "__base__/graphics/entity/rocket-silo/rocket-static-pod.png", "signal-R"); + rocketLaunch = createSpecialObject(false, SpecialNames.RocketLaunch, LSs.SpecialObjectLaunchSlot, + LSs.SpecialObjectLaunchSlotDescription, "__base__/graphics/entity/rocket-silo/rocket-static-pod.png", "signal-R"); science = GetObject("science"); science.showInExplorers = false; Analysis.ExcludeFromAnalysis(science); formerAliases["Special.research-unit"] = science; - generatorProduction = CreateSpecialRecipe(electricity, SpecialNames.GeneratorRecipe, "generating"); + generatorProduction = CreateSpecialRecipe(electricity, SpecialNames.GeneratorRecipe, LSs.SpecialRecipeGenerating); generatorProduction.products = [new Product(electricity, 1f)]; generatorProduction.flags |= RecipeFlags.ScaleProductionWithPower; generatorProduction.ingredients = []; - reactorProduction = CreateSpecialRecipe(heat, SpecialNames.ReactorRecipe, "generating"); + reactorProduction = CreateSpecialRecipe(heat, SpecialNames.ReactorRecipe, LSs.SpecialRecipeGenerating); reactorProduction.products = [new Product(heat, 1f)]; reactorProduction.flags |= RecipeFlags.ScaleProductionWithPower; reactorProduction.ingredients = []; @@ -105,8 +106,8 @@ Item createSpecialItem(string name, string locName, string locDescr, string icon laborEntityEnergy = new EntityEnergy { type = EntityEnergyType.Labor, effectivity = float.PositiveInfinity }; // Note: These must be Items (or possibly a derived type) so belt capacity can be displayed and set. - totalItemInput = createSpecialItem("item-total-input", "Total item consumption", "This item represents the combined total item input of a multi-ingredient recipe. It can be used to set or measure the number of sushi belts required to supply this recipe row.", "__base__/graphics/icons/signal/signal_I.png"); - totalItemOutput = createSpecialItem("item-total-output", "Total item production", "This item represents the combined total item output of a multi-product recipe. It can be used to set or measure the number of sushi belts required to handle the products of this recipe row.", "__base__/graphics/icons/signal/signal_O.png"); + totalItemInput = createSpecialItem("item-total-input", LSs.SpecialItemTotalConsumption, LSs.SpecialItemTotalConsumptionDescription, "__base__/graphics/icons/signal/signal_I.png"); + totalItemOutput = createSpecialItem("item-total-output", LSs.SpecialItemTotalProduction, LSs.SpecialItemTotalProductionDescription, "__base__/graphics/icons/signal/signal_O.png"); formerAliases["Special.total-item-input"] = totalItemInput; formerAliases["Special.total-item-output"] = totalItemOutput; } @@ -471,7 +472,7 @@ private void CalculateMaps(bool netProduction) { switch (o) { case RecipeOrTechnology recipeOrTechnology: if (recipeOrTechnology is Recipe recipe) { - recipe.FallbackLocalization(recipe.mainProduct, "A recipe to create"); + recipe.FallbackLocalization(recipe.mainProduct, LSs.LocalizationFallbackDescriptionRecipeToCreate); recipe.technologyUnlock = recipeUnlockers.GetArray(recipe); } @@ -484,16 +485,15 @@ private void CalculateMaps(bool netProduction) { if (o is Item item) { if (item.placeResult != null) { - item.FallbackLocalization(item.placeResult, "An item to build"); + item.FallbackLocalization(item.placeResult, LSs.LocalizationFallbackDescriptionItemToBuild); } } else if (o is Fluid fluid && fluid.variants != null) { - string temperatureDescr = "Temperature: " + fluid.temperature + "°"; if (fluid.locDescr == null) { - fluid.locDescr = temperatureDescr; + fluid.locDescr = LSs.FluidDescriptionTemperatureSolo.L(fluid.temperature); } else { - fluid.locDescr = temperatureDescr + "\n" + fluid.locDescr; + fluid.locDescr = LSs.FluidDescriptionTemperatureAdded.L(fluid.temperature, fluid.locDescr); } } @@ -560,7 +560,7 @@ private void CalculateMaps(bool netProduction) { } foreach (var mechanic in allMechanics) { - mechanic.locName = mechanic.source.locName + " " + mechanic.locName; + mechanic.locName = mechanic.localizationKey.Localize(mechanic.source.locName, mechanic.products.FirstOrDefault()?.goods.fluid?.temperature!); mechanic.locDescr = mechanic.source.locDescr; mechanic.iconSpec = mechanic.source.iconSpec; } @@ -643,7 +643,7 @@ private void CalculateMaps(bool netProduction) { foreach (var (_, list) in fluidVariants) { foreach (var fluid in list) { - fluid.locName += " " + fluid.temperature + "°"; + fluid.locName = LSs.FluidNameWithTemperature.L(fluid.locName, fluid.temperature); } } @@ -654,7 +654,7 @@ static int countNonDsrRecipes(IEnumerable recipes) && r.specialType is not FactorioObjectSpecialType.Recycling and not FactorioObjectSpecialType.Voiding); } - private Recipe CreateSpecialRecipe(FactorioObject production, string category, string hint) { + private Recipe CreateSpecialRecipe(FactorioObject production, string category, LocalizableString specialRecipeKey) { string fullName = category + (category.EndsWith('.') ? "" : ".") + production.name; if (registeredObjects.TryGetValue((typeof(Mechanics), fullName), out var recipeRaw)) { @@ -666,7 +666,7 @@ private Recipe CreateSpecialRecipe(FactorioObject production, string category, s recipe.factorioType = SpecialNames.FakeRecipe; recipe.name = fullName; recipe.source = production; - recipe.locName = hint; + recipe.localizationKey = specialRecipeKey; recipe.enabled = true; recipe.hidden = true; recipe.technologyUnlock = []; diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs index 98a2e59f..5b789665 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_Entity.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -152,7 +153,7 @@ private static void ParseModules(LuaTable table, EntityWithModules entity, Allow private Recipe CreateLaunchRecipe(EntityCrafter entity, Recipe recipe, int partsRequired, int outputCount) { string launchCategory = SpecialNames.RocketCraft + entity.name; - var launchRecipe = CreateSpecialRecipe(recipe, launchCategory, "launch"); + var launchRecipe = CreateSpecialRecipe(recipe, launchCategory, LSs.SpecialRecipeLaunch); recipeCrafters.Add(entity, launchCategory); launchRecipe.ingredients = [.. recipe.products.Select(x => new Ingredient(x.goods, x.amount * partsRequired))]; launchRecipe.products = [new Product(rocketLaunch, outputCount)]; @@ -265,7 +266,7 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { // otherwise convert boiler production to a recipe string category = SpecialNames.BoilerRecipe + boiler.name; - var recipe = CreateSpecialRecipe(output, category, "boiling to " + targetTemp + "°"); + var recipe = CreateSpecialRecipe(output, category, LSs.SpecialRecipeBoiling); recipeCrafters.Add(boiler, category); recipe.flags |= RecipeFlags.UsesFluidTemperature; // TODO: input fluid amount now depends on its temperature, using min temperature should be OK for non-modded @@ -466,7 +467,7 @@ private void DeserializeEntity(LuaTable table, ErrorCollector errorCollector) { if (table.Get("fluid_box", out LuaTable? fluidBox) && fluidBox.Get("fluid", out string? fluidName)) { var pumpingFluid = GetFluidFixedTemp(fluidName, 0); string recipeCategory = SpecialNames.PumpingRecipe + pumpingFluid.name; - recipe = CreateSpecialRecipe(pumpingFluid, recipeCategory, "pumping"); + recipe = CreateSpecialRecipe(pumpingFluid, recipeCategory, LSs.SpecialRecipePumping); recipeCrafters.Add(pump, recipeCategory); pump.energy = voidEntityEnergy; @@ -545,7 +546,7 @@ void parseEffect(LuaTable effect) { if (factorioType == "resource") { // mining resource is processed as a recipe _ = table.Get("category", out string category, "basic-solid"); - var recipe = CreateSpecialRecipe(entity, SpecialNames.MiningRecipe + category, "mining"); + var recipe = CreateSpecialRecipe(entity, SpecialNames.MiningRecipe + category, LSs.SpecialRecipeMining); recipe.flags = RecipeFlags.UsesMiningProductivity; recipe.time = minable.Get("mining_time", 1f); recipe.products = products; @@ -568,7 +569,7 @@ void parseEffect(LuaTable effect) { else if (factorioType == "plant") { // harvesting plants is processed as a recipe foreach (var seed in plantResults.Where(x => x.Value == name).Select(x => x.Key)) { - var recipe = CreateSpecialRecipe(seed, SpecialNames.PlantRecipe, "planting"); + var recipe = CreateSpecialRecipe(seed, SpecialNames.PlantRecipe, LSs.SpecialRecipePlanting); recipe.time = table.Get("growth_ticks", 0) / 60f; recipe.ingredients = [new Ingredient(seed, 1)]; recipe.products = products; @@ -642,7 +643,7 @@ private void DeserializeAsteroidChunk(LuaTable table, ErrorCollector errorCollec Entity chunk = DeserializeCommon(table, "asteroid-chunk"); Item asteroid = GetObject(chunk.name); if (asteroid.showInExplorers) { // don't create mining recipes for parameter chunks. - Recipe recipe = CreateSpecialRecipe(asteroid, SpecialNames.AsteroidCapture, "mining"); + Recipe recipe = CreateSpecialRecipe(asteroid, SpecialNames.AsteroidCapture, LSs.SpecialRecipeMining); recipe.time = 1; recipe.ingredients = []; recipe.products = [new Product(asteroid, 1)]; diff --git a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs index c3d996c9..221c7ca3 100644 --- a/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs +++ b/Yafc.Parser/Data/FactorioDataDeserializer_RecipeAndTechnology.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Yafc.I18n; using Yafc.Model; namespace Yafc.Parser; @@ -354,7 +355,7 @@ private void LoadResearchTrigger(LuaTable researchTriggerTable, ref Technology t EnsureLaunchRecipe(item, null); break; default: - errorCollector.Error($"Research trigger of {technology.typeDotName} has an unsupported type {type}", ErrorSeverity.MinorDataLoss); + errorCollector.Error(LSs.ResearchHasAnUnsupportedTriggerType.L(technology.typeDotName, type), ErrorSeverity.MinorDataLoss); break; } } diff --git a/Yafc.Parser/FactorioDataSource.cs b/Yafc.Parser/FactorioDataSource.cs index 51cd6723..50be04a2 100644 --- a/Yafc.Parser/FactorioDataSource.cs +++ b/Yafc.Parser/FactorioDataSource.cs @@ -137,7 +137,7 @@ private static void FindMods(string directory, IProgress<(string, string)> progr string infoFile = Path.Combine(entry, "info.json"); if (File.Exists(infoFile)) { - progress.Report(("Initializing", entry)); + progress.Report((LSs.ProgressInitializing, entry)); ModInfo info = new(entry, File.ReadAllBytes(infoFile)); mods.Add(info); } @@ -190,13 +190,13 @@ public static Project Parse(string factorioPath, string modPath, string projectP try { CurrentLoadingMod = null; string modSettingsPath = Path.Combine(modPath, "mod-settings.dat"); - progress.Report(("Initializing", "Loading mod list")); + progress.Report((LSs.ProgressInitializing, LSs.ProgressLoadingModList)); string modListPath = Path.Combine(modPath, "mod-list.json"); Dictionary versionSpecifiers = []; bool hasModList = File.Exists(modListPath); if (hasModList) { - var mods = JsonSerializer.Deserialize(File.ReadAllText(modListPath)) ?? throw new($"Could not read mod list from {modListPath}"); + var mods = JsonSerializer.Deserialize(File.ReadAllText(modListPath)) ?? throw new(LSs.CouldNotReadModList.L(modListPath)); allMods = mods.mods.Where(x => x.enabled).Select(x => x.name).ToDictionary(x => x, x => (ModInfo)null!); versionSpecifiers = mods.mods.Where(x => x.enabled && !string.IsNullOrEmpty(x.version)).ToDictionary(x => x.name, x => Version.Parse(x.version!)); // null-forgiving: null version strings are filtered by the Where. } @@ -274,7 +274,7 @@ public static Project Parse(string factorioPath, string modPath, string projectP } if (missingMod != null) { - throw new NotSupportedException("Mod not found: " + missingMod + ". Try loading this pack in Factorio first."); + throw new NotSupportedException(LSs.ModNotFoundTryInFactorio.L(missingMod)); } List modsToDisable = []; @@ -298,7 +298,7 @@ public static Project Parse(string factorioPath, string modPath, string projectP } while (modsToDisable.Count > 0); CurrentLoadingMod = null; - progress.Report(("Initializing", "Creating Lua context")); + progress.Report((LSs.ProgressInitializing, LSs.ProgressCreatingLuaContext)); HashSet modsToLoad = [.. allMods.Keys]; string[] modLoadOrder = new string[modsToLoad.Count]; @@ -318,7 +318,7 @@ public static Project Parse(string factorioPath, string modPath, string projectP } } if (currentLoadBatch.Count == 0) { - throw new NotSupportedException("Mods dependencies are circular. Unable to load mods: " + string.Join(", ", modsToLoad)); + throw new NotSupportedException(LSs.CircularModDependencies.L(string.Join(LSs.ListSeparator, modsToLoad))); } foreach (string mod in currentLoadBatch) { @@ -376,7 +376,7 @@ public static Project Parse(string factorioPath, string modPath, string projectP FactorioDataDeserializer deserializer = new FactorioDataDeserializer(factorioVersion ?? defaultFactorioVersion); var project = deserializer.LoadData(projectPath, dataContext.data, (LuaTable)dataContext.defines["prototypes"]!, netProduction, progress, errorCollector, renderIcons, useLatestSave); logger.Information("Completed!"); - progress.Report(("Completed!", "")); + progress.Report((LSs.ProgressCompleted, "")); return project; } diff --git a/Yafc.Parser/LuaContext.cs b/Yafc.Parser/LuaContext.cs index a662e8ca..db33bea7 100644 --- a/Yafc.Parser/LuaContext.cs +++ b/Yafc.Parser/LuaContext.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using Serilog; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Yafc.Model.Tests")] @@ -549,7 +551,7 @@ public void Dispose() { } public void DoModFiles(string[] modorder, string fileName, IProgress<(string, string)> progress) { - string header = "Executing mods " + fileName; + string header = LSs.ProgressExecutingModAtDataStage.L(fileName); foreach (string mod in modorder) { required.Clear(); @@ -570,7 +572,7 @@ public void DoModFiles(string[] modorder, string fileName, IProgress<(string, st public LuaTable defines => (LuaTable)GetGlobal("defines")!; } -internal class LuaTable { +internal class LuaTable : ILocalizable { public readonly LuaContext context; public readonly int refId; @@ -590,4 +592,9 @@ public object? this[string index] { public List ArrayElements => context.ArrayElements(refId); public Dictionary ObjectElements => context.ObjectElements(refId); + + bool ILocalizable.Get([NotNullWhen(true)] out string? key, out object[] parameters) { + parameters = ArrayElements.Skip(1).ToArray()!; + return this.Get(1, out key); + } } diff --git a/Yafc.UI/Core/ExceptionScreen.cs b/Yafc.UI/Core/ExceptionScreen.cs index b97c101d..d6a0ed13 100644 --- a/Yafc.UI/Core/ExceptionScreen.cs +++ b/Yafc.UI/Core/ExceptionScreen.cs @@ -1,6 +1,7 @@ using System; using SDL2; using Serilog; +using Yafc.I18n; namespace Yafc.UI; @@ -41,15 +42,15 @@ protected override void BuildContents(ImGui gui) { gui.BuildText(ex.StackTrace, TextBlockDisplayStyle.WrappedText); using (gui.EnterRow(0.5f, RectAllocator.RightRow)) { - if (gui.BuildButton("Close")) { + if (gui.BuildButton(LSs.Close)) { Close(); } - if (gui.BuildButton("Ignore future errors", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.IgnoreFutureErrors, SchemeColor.Grey)) { ignoreAll = true; Close(); } - if (gui.BuildButton("Copy to clipboard", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.CopyToClipboard, SchemeColor.Grey)) { _ = SDL.SDL_SetClipboardText(ex.Message + "\n\n" + ex.StackTrace); } } diff --git a/Yafc.UI/ImGui/ImGuiUtils.cs b/Yafc.UI/ImGui/ImGuiUtils.cs index 0c0de75e..3082896e 100644 --- a/Yafc.UI/ImGui/ImGuiUtils.cs +++ b/Yafc.UI/ImGui/ImGuiUtils.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using SDL2; +using Yafc.I18n; namespace Yafc.UI; @@ -263,18 +264,18 @@ public static ButtonEvent BuildRadioButton(this ImGui gui, string option, bool s return click; } - public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList options, int selected, out int newSelected, + public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList options, int selected, out int newSelected, SchemeColor textColor = SchemeColor.None, bool enabled = true) - => gui.BuildRadioGroup([.. options.Select(o => (o, (string?)null))], selected, out newSelected, textColor, enabled); + => gui.BuildRadioGroup([.. options.Select(o => (o, (LocalizableString0?)null))], selected, out newSelected, textColor, enabled); - public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList<(string option, string? tooltip)> options, int selected, + public static bool BuildRadioGroup(this ImGui gui, IReadOnlyList<(LocalizableString0 option, LocalizableString0? tooltip)> options, int selected, out int newSelected, SchemeColor textColor = SchemeColor.None, bool enabled = true) { newSelected = selected; for (int i = 0; i < options.Count; i++) { ButtonEvent evt = BuildRadioButton(gui, options[i].option, selected == i, textColor, enabled); - if (!string.IsNullOrEmpty(options[i].tooltip)) { + if (options[i].tooltip != null) { _ = evt.WithTooltip(gui, options[i].tooltip!); } if (evt) { @@ -434,9 +435,10 @@ public static bool BuildSlider(this ImGui gui, float value, out float newValue, return true; } - public static bool BuildSearchBox(this ImGui gui, SearchQuery searchQuery, out SearchQuery newQuery, string placeholder = "Search", SetKeyboardFocus setKeyboardFocus = SetKeyboardFocus.No) { + public static bool BuildSearchBox(this ImGui gui, SearchQuery searchQuery, out SearchQuery newQuery, string? placeholder = null, SetKeyboardFocus setKeyboardFocus = SetKeyboardFocus.No) { newQuery = searchQuery; + placeholder ??= LSs.SearchHint; if (gui.BuildTextInput(searchQuery.query, out string newText, placeholder, Icon.Search, setKeyboardFocus: setKeyboardFocus)) { newQuery = new SearchQuery(newText); return true; diff --git a/Yafc.UI/Yafc.UI.csproj b/Yafc.UI/Yafc.UI.csproj index 2bd0fd38..e33fb37a 100644 --- a/Yafc.UI/Yafc.UI.csproj +++ b/Yafc.UI/Yafc.UI.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/Yafc/Program.cs b/Yafc/Program.cs index c3a4950f..18b03570 100644 --- a/Yafc/Program.cs +++ b/Yafc/Program.cs @@ -1,6 +1,8 @@ using System; using System.IO; +using Yafc.I18n; using Yafc.Model; +using Yafc.Parser; using Yafc.UI; namespace Yafc; @@ -11,6 +13,10 @@ public static class Program { private static void Main(string[] args) { YafcLib.RegisterDefaultAnalysis(); Ui.Start(); + + // This must happen before Preferences.Instance, where we load the prefs file and the requested translation. + FactorioDataSource.LoadYafcLocale("en"); + string? overrideFont = Preferences.Instance.overrideFont; FontFile? overriddenFontFile = null; @@ -40,11 +46,11 @@ private static void Main(string[] args) { ProjectDefinition? cliProject = CommandLineParser.ParseArgs(args); if (CommandLineParser.errorOccured || CommandLineParser.helpRequested) { - Console.WriteLine("YAFC CE v" + YafcLib.version.ToString(3)); + Console.WriteLine(LSs.YafcWithVersion.L(YafcLib.version.ToString(3))); Console.WriteLine(); if (CommandLineParser.errorOccured) { - Console.WriteLine($"Error: {CommandLineParser.lastError}"); + Console.WriteLine(LSs.CommandLineError.L(CommandLineParser.lastError)); Console.WriteLine(); Environment.ExitCode = 1; } diff --git a/Yafc/Utils/CommandLineParser.cs b/Yafc/Utils/CommandLineParser.cs index af9efd3b..85b89a83 100644 --- a/Yafc/Utils/CommandLineParser.cs +++ b/Yafc/Utils/CommandLineParser.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using Yafc.I18n; namespace Yafc; @@ -30,7 +31,7 @@ public static class CommandLineParser { if (!args[0].StartsWith("--")) { projectDefinition.dataPath = args[0]; if (!Directory.Exists(projectDefinition.dataPath)) { - lastError = $"Data path '{projectDefinition.dataPath}' does not exist."; + lastError = LSs.CommandLineErrorPathDoesNotExist.L(LSs.FolderTypeData, projectDefinition.modsPath); return null; } } @@ -42,12 +43,12 @@ public static class CommandLineParser { projectDefinition.modsPath = args[++i]; if (!Directory.Exists(projectDefinition.modsPath)) { - lastError = $"Mods path '{projectDefinition.modsPath}' does not exist."; + lastError = LSs.CommandLineErrorPathDoesNotExist.L(LSs.FolderTypeMods, projectDefinition.modsPath); return null; } } else { - lastError = "Missing argument for --mods-path."; + lastError = LSs.MissingCommandLineArgument.L("--mods-path"); return null; } break; @@ -58,12 +59,12 @@ public static class CommandLineParser { string? directory = Path.GetDirectoryName(projectDefinition.path); if (!Directory.Exists(directory)) { - lastError = $"Project directory for '{projectDefinition.path}' does not exist."; + lastError = LSs.CommandLineErrorPathDoesNotExist.L(LSs.FolderTypeProject, projectDefinition.path); return null; } } else { - lastError = "Missing argument for --project-file."; + lastError = LSs.MissingCommandLineArgument.L("--project-file"); return null; } break; @@ -77,7 +78,7 @@ public static class CommandLineParser { break; default: - lastError = $"Unknown argument '{args[i]}'."; + lastError = LSs.CommandLineErrorUnknownArgument.L(args[i]); return null; } } @@ -85,52 +86,7 @@ public static class CommandLineParser { return projectDefinition; } - public static void PrintHelp() => Console.WriteLine(@"Usage: -Yafc [ [--mods-path ] [--project-file ] [--help] - -Description: - Yafc can be started without any arguments. However, if arguments are supplied, it is - mandatory that the first argument is the path to the data directory of Factorio. The - other arguments are optional in any case. - -Options: - - Path of the data directory (mandatory if other arguments are supplied) - - --mods-path - Path of the mods directory (optional) - - --project-file - Path of the project file (optional) - - --help - Display this help message and exit - -Examples: - 1. Starting Yafc without any arguments: - $ ./Yafc - This opens the welcome screen. - - 2. Starting Yafc with a project path: - $ ./Yafc path/to/my/project.yafc - Skips the welcome screen and loads the project. If the project has not been - opened before, then uses the start-settings of the most-recently-opened project. - - 3. Starting Yafc with the path to the data directory of Factorio: - $ ./Yafc Factorio/data - This opens a fresh project and loads the game data from the supplied directory. - Fails if the directory does not exist. - - 4. Starting Yafc with the paths to the data directory and a project file: - $ ./Yafc Factorio/data --project-file my-project.yafc - This opens the supplied project and loads the game data from the supplied data - directory. Fails if the directory and/or the project file do not exist. - - 5. Starting Yafc with the paths to the data & mods directories and a project file: - $ ./Yafc Factorio/data --mods-path Factorio/mods --project-file my-project.yafc - This opens the supplied project and loads the game data and mods from the supplied - data and mods directories. Fails if any of the directories and/or the project file - do not exist."); + public static void PrintHelp() => Console.WriteLine(LSs.ConsoleHelpMessage); /// /// Loads the project from the given path.
diff --git a/Yafc/Widgets/ImmediateWidgets.cs b/Yafc/Widgets/ImmediateWidgets.cs index 572bb0ca..fb4c64f0 100644 --- a/Yafc/Widgets/ImmediateWidgets.cs +++ b/Yafc/Widgets/ImmediateWidgets.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using SDL2; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -191,7 +192,7 @@ public static Click BuildFactorioObjectButtonWithText(this ImGui gui, IFactorioO gui.BuildText(extraText, TextBlockDisplayStyle.Default(color)); } _ = gui.RemainingRow(); - gui.BuildText(obj == null ? "None" : obj.target.locName, TextBlockDisplayStyle.WrappedText with { Color = color }); + gui.BuildText(obj == null ? LSs.FactorioObjectNone : obj.target.locName, TextBlockDisplayStyle.WrappedText with { Color = color }); } return gui.BuildFactorioObjectButtonBackground(gui.lastRect, obj, tooltipOptions: tooltipOptions); @@ -236,7 +237,7 @@ public static void BuildInlineObjectListAndButton(this ImGui gui, ICollection } } - if (list.Count > options.MaxCount && gui.BuildButton("See full list") && gui.CloseDropdown()) { + if (list.Count > options.MaxCount && gui.BuildButton(LSs.SeeFullListButton) && gui.CloseDropdown()) { if (options.Multiple) { SelectMultiObjectPanel.Select(list, options.Header, selectItem, options.Ordering, options.Checkmark, options.YellowMark); } @@ -253,11 +254,11 @@ public static void BuildInlineObjectListAndButtonWithNone(this ImGui gui, ICo selectItem(selected); _ = gui.CloseDropdown(); } - if (gui.BuildRedButton("Clear") && gui.CloseDropdown()) { + if (gui.BuildRedButton(LSs.ClearButton) && gui.CloseDropdown()) { selectItem(null); } - if (list.Count > options.MaxCount && gui.BuildButton("See full list") && gui.CloseDropdown()) { + if (list.Count > options.MaxCount && gui.BuildButton(LSs.SeeFullListButton) && gui.CloseDropdown()) { SelectSingleObjectPanel.SelectWithNone(list, options.Header, selectItem, options.Ordering); } } @@ -293,14 +294,13 @@ public static void ShowPrecisionValueTooltip(ImGui gui, DisplayAmount amount, IF case UnitOfMeasure.PerSecond: case UnitOfMeasure.FluidPerSecond: case UnitOfMeasure.ItemPerSecond: - string perSecond = DataUtils.FormatAmountRaw(amount.Value, 1f, "/s", DataUtils.PreciseFormat); - string perMinute = DataUtils.FormatAmountRaw(amount.Value, 60f, "/m", DataUtils.PreciseFormat); - string perHour = DataUtils.FormatAmountRaw(amount.Value, 3600f, "/h", DataUtils.PreciseFormat); + string perSecond = DataUtils.FormatAmountRaw(amount.Value, 1f, LSs.PerSecondSuffix, DataUtils.PreciseFormat); + string perMinute = DataUtils.FormatAmountRaw(amount.Value, 60f, LSs.PerMinuteSuffix, DataUtils.PreciseFormat); + string perHour = DataUtils.FormatAmountRaw(amount.Value, 3600f, LSs.PerHourSuffix, DataUtils.PreciseFormat); text = perSecond + "\n" + perMinute + "\n" + perHour; if (goods.target is Item item) { - text += "\n"; - text += DataUtils.FormatAmount(MathF.Abs(item.stackSize / amount.Value), UnitOfMeasure.Second) + " per stack"; + text += "\n" + LSs.SecondsPerStack.L(DataUtils.FormatAmount(MathF.Abs(item.stackSize / amount.Value), UnitOfMeasure.Second)); } break; @@ -386,19 +386,21 @@ public static GoodsWithAmountEvent BuildFactorioObjectWithEditableAmount(this Im ///
/// The to initially display selected, if any. /// The selected by the user. - /// The header text to draw, defaults to "Select quality" + /// The localizable string for the text to draw, defaults to /// if the user selected a quality. if they did not, or if the loaded mods do not provide multiple qualities. - public static bool BuildQualityList(this ImGui gui, Quality? quality, [NotNullWhen(true), NotNullIfNotNull(nameof(quality))] out Quality? newQuality, string header = "Select quality", bool drawCentered = false) { + public static bool BuildQualityList(this ImGui gui, Quality? quality, [NotNullWhen(true), NotNullIfNotNull(nameof(quality))] out Quality? newQuality, LocalizableString0? headerKey = null, bool drawCentered = false) { newQuality = quality; if (Quality.Normal.nextQuality == null) { return false; // Nothing to do; normal quality is the only one defined. } + headerKey ??= LSs.SelectQuality; + if (drawCentered) { - gui.BuildText(header, TextBlockDisplayStyle.Centered with { Font = Font.productionTableHeader }); + gui.BuildText(headerKey, TextBlockDisplayStyle.Centered with { Font = Font.productionTableHeader }); } else { - gui.BuildText(header, Font.productionTableHeader); + gui.BuildText(headerKey, Font.productionTableHeader); } using ImGui.OverlappingAllocations controller = gui.StartOverlappingAllocations(false); diff --git a/Yafc/Widgets/MainScreenTabBar.cs b/Yafc/Widgets/MainScreenTabBar.cs index 4e5559ae..380d7421 100644 --- a/Yafc/Widgets/MainScreenTabBar.cs +++ b/Yafc/Widgets/MainScreenTabBar.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; using SDL2; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -108,23 +109,23 @@ private void BuildContents(ImGui gui) { private void PageRightClickDropdown(ImGui gui, ProjectPage page) { bool isSecondary = screen.secondaryPage == page; bool isActive = screen.activePage == page; - if (gui.BuildContextMenuButton("Edit properties")) { + if (gui.BuildContextMenuButton(LSs.EditPageProperties)) { _ = gui.CloseDropdown(); ProjectPageSettingsPanel.Show(page); } if (!isSecondary && !isActive) { - if (gui.BuildContextMenuButton("Open as secondary", "Ctrl+Click")) { + if (gui.BuildContextMenuButton(LSs.OpenSecondaryPage, LSs.ShortcutCtrlClick)) { _ = gui.CloseDropdown(); screen.SetSecondaryPage(page); } } else if (isSecondary) { - if (gui.BuildContextMenuButton("Close secondary", "Ctrl+Click")) { + if (gui.BuildContextMenuButton(LSs.CloseSecondaryPage, LSs.ShortcutCtrlClick)) { _ = gui.CloseDropdown(); screen.SetSecondaryPage(null); } } - if (gui.BuildContextMenuButton("Duplicate")) { + if (gui.BuildContextMenuButton(LSs.DuplicatePage)) { _ = gui.CloseDropdown(); if (ProjectPageSettingsPanel.ClonePage(page) is { } copy) { screen.project.RecordUndo().pages.Add(copy); diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index e51752d1..f120102e 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -39,7 +40,7 @@ private void BuildHeader(ImGui gui) { using (gui.EnterGroup(new Padding(1f, 0.5f), RectAllocator.LeftAlign, spacing: 0f)) { string name = target.text; if (tooltipOptions.ShowTypeInHeader && target is not Goods) { - name = name + " (" + target.target.type + ")"; + name = LSs.NameWithType.L(name, target.target.type); } gui.BuildText(name, new TextBlockDisplayStyle(Font.header, true)); @@ -101,7 +102,7 @@ private static void BuildIconRow(ImGui gui, IReadOnlyList EnergyDescriptions = new Dictionary + private static readonly Dictionary EnergyDescriptions = new() { - {EntityEnergyType.Electric, "Power usage: "}, - {EntityEnergyType.Heat, "Heat energy usage: "}, - {EntityEnergyType.Labor, "Labor energy usage: "}, - {EntityEnergyType.Void, "Free energy usage: "}, - {EntityEnergyType.FluidFuel, "Fluid fuel energy usage: "}, - {EntityEnergyType.FluidHeat, "Fluid heat energy usage: "}, - {EntityEnergyType.SolidFuel, "Solid fuel energy usage: "}, + {EntityEnergyType.Electric, LSs.EnergyElectricity}, + {EntityEnergyType.Heat, LSs.EnergyHeat}, + {EntityEnergyType.Labor, LSs.EnergyLabor}, + {EntityEnergyType.Void, LSs.EnergyFree}, + {EntityEnergyType.FluidFuel, LSs.EnergyFluidFuel}, + {EntityEnergyType.FluidHeat, LSs.EnergyFluidHeat}, + {EntityEnergyType.SolidFuel, LSs.EnergySolidFuel}, }; private void BuildEntity(Entity entity, Quality quality, ImGui gui) { if (entity.loot.Length > 0) { - BuildSubHeader(gui, "Loot"); + BuildSubHeader(gui, LSs.TooltipHeaderLoot); using (gui.EnterGroup(contentPadding)) { foreach (var product in entity.loot) { BuildItem(gui, product); @@ -228,42 +229,43 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { if (entity.mapGenerated) { using (gui.EnterGroup(contentPadding)) { - gui.BuildText("Generates on map (estimated density: " + (entity.mapGenDensity <= 0f ? "unknown" : DataUtils.FormatAmount(entity.mapGenDensity, UnitOfMeasure.None)) + ")", + gui.BuildText(entity.mapGenDensity <= 0 ? LSs.MapGenerationDensityUnknown + : LSs.MapGenerationDensity.L(DataUtils.FormatAmount(entity.mapGenDensity, UnitOfMeasure.None)), TextBlockDisplayStyle.WrappedText); } } if (entity is EntityCrafter crafter) { if (crafter.recipes.Length > 0) { - BuildSubHeader(gui, "Crafts"); + BuildSubHeader(gui, LSs.EntityCrafts); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, crafter.recipes, 2); if (crafter.CraftingSpeed(quality) != 1f) { - gui.BuildText("Crafting speed: " + DataUtils.FormatAmount(crafter.CraftingSpeed(quality), UnitOfMeasure.Percent)); + gui.BuildText(LSs.EntityCraftingSpeed.L(DataUtils.FormatAmount(crafter.CraftingSpeed(quality), UnitOfMeasure.Percent))); } Effect baseEffect = crafter.effectReceiver.baseEffect; if (baseEffect.speed != 0f) { - gui.BuildText("Crafting speed: " + DataUtils.FormatAmount(baseEffect.speed, UnitOfMeasure.Percent)); + gui.BuildText(LSs.EntityCraftingSpeed.L(DataUtils.FormatAmount(baseEffect.speed, UnitOfMeasure.Percent))); } if (baseEffect.productivity != 0f) { - gui.BuildText("Crafting productivity: " + DataUtils.FormatAmount(baseEffect.productivity, UnitOfMeasure.Percent)); + gui.BuildText(LSs.EntityCraftingProductivity.L(DataUtils.FormatAmount(baseEffect.productivity, UnitOfMeasure.Percent))); } if (baseEffect.consumption != 0f) { - gui.BuildText("Energy consumption: " + DataUtils.FormatAmount(baseEffect.consumption, UnitOfMeasure.Percent)); + gui.BuildText(LSs.EntityEnergyConsumption.L(DataUtils.FormatAmount(baseEffect.consumption, UnitOfMeasure.Percent))); } if (crafter.allowedEffects != AllowedEffects.None) { - gui.BuildText("Module slots: " + crafter.moduleSlots); + gui.BuildText(LSs.EntityModuleSlots.L(crafter.moduleSlots)); if (crafter.allowedEffects != AllowedEffects.All) { - gui.BuildText("Only allowed effects: " + crafter.allowedEffects, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.AllowedModuleEffectsUntranslatedList.L(crafter.allowedEffects), TextBlockDisplayStyle.WrappedText); } } } } if (crafter.inputs != null) { - BuildSubHeader(gui, "Allowed inputs:"); + BuildSubHeader(gui, LSs.LabAllowedInputs); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, crafter.inputs, 2); } @@ -272,23 +274,23 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { float spoilTime = entity.GetSpoilTime(quality); // The spoiling rate setting does not apply to entities. if (spoilTime != 0f) { - BuildSubHeader(gui, "Perishable"); + BuildSubHeader(gui, LSs.Perishable); using (gui.EnterGroup(contentPadding)) { if (entity.spoilResult != null) { - gui.BuildText($"After {DataUtils.FormatTime(spoilTime)} of no production, spoils into"); + gui.BuildText(LSs.TooltipEntitySpoilsAfterNoProduction.L(DataUtils.FormatTime(spoilTime))); gui.BuildFactorioObjectButtonWithText(entity.spoilResult.With(quality), iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); } else { - gui.BuildText($"Expires after {DataUtils.FormatTime(spoilTime)} of no production"); + gui.BuildText(LSs.TooltipEntityExpiresAfterNoProduction.L(DataUtils.FormatTime(spoilTime))); } tooltipOptions.ExtraSpoilInformation?.Invoke(gui); } } if (entity.energy != null) { - string energyUsage = EnergyDescriptions[entity.energy.type] + DataUtils.FormatAmount(entity.Power(quality), UnitOfMeasure.Megawatt); + string energyUsage = EnergyDescriptions[entity.energy.type].L(DataUtils.FormatAmount(entity.Power(quality), UnitOfMeasure.Megawatt)); if (entity.energy.drain > 0f) { - energyUsage += " + " + DataUtils.FormatAmount(entity.energy.drain, UnitOfMeasure.Megawatt); + energyUsage = LSs.TooltipAddDrainEnergy.L(energyUsage, DataUtils.FormatAmount(entity.energy.drain, UnitOfMeasure.Megawatt)); } BuildSubHeader(gui, energyUsage); @@ -302,15 +304,15 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { TextBlockDisplayStyle emissionStyle = TextBlockDisplayStyle.Default(SchemeColor.BackgroundText); if (amount < 0f) { emissionStyle = TextBlockDisplayStyle.Default(SchemeColor.Green); - gui.BuildText("This building absorbs " + name, emissionStyle); - gui.BuildText($"Absorption: {DataUtils.FormatAmount(-amount, UnitOfMeasure.None)} {name} per minute", emissionStyle); + gui.BuildText(LSs.EntityAbsorbsPollution.L(name), emissionStyle); + gui.BuildText(LSs.TooltipEntityAbsorbsPollution.L(DataUtils.FormatAmount(-amount, UnitOfMeasure.None), name), emissionStyle); } else { if (amount >= 20f) { emissionStyle = TextBlockDisplayStyle.Default(SchemeColor.Error); - gui.BuildText("This building contributes to global warning!", emissionStyle); + gui.BuildText(LSs.EntityHasHighPollution, emissionStyle); } - gui.BuildText($"Emission: {DataUtils.FormatAmount(amount, UnitOfMeasure.None)} {name} per minute", emissionStyle); + gui.BuildText(LSs.TooltipEntityEmitsPollution.L(DataUtils.FormatAmount(amount, UnitOfMeasure.None), name), emissionStyle); } } } @@ -321,7 +323,7 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { using (gui.EnterGroup(contentPadding)) using (gui.EnterRow(0)) { gui.AllocateRect(0, 1.5f); - gui.BuildText($"Requires {DataUtils.FormatAmount(entity.heatingPower, UnitOfMeasure.Megawatt)} heat on cold planets."); + gui.BuildText(LSs.TooltipEntityRequiresHeat.L(DataUtils.FormatAmount(entity.heatingPower, UnitOfMeasure.Megawatt))); } } @@ -329,29 +331,26 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { switch (entity) { case EntityBelt belt: - miscText = "Belt throughput (Items): " + DataUtils.FormatAmount(belt.beltItemsPerSecond, UnitOfMeasure.PerSecond); + miscText = LSs.BeltThroughput.L(DataUtils.FormatAmount(belt.beltItemsPerSecond, UnitOfMeasure.PerSecond)); break; case EntityInserter inserter: - miscText = "Swing time: " + DataUtils.FormatAmount(inserter.inserterSwingTime, UnitOfMeasure.Second); + miscText = LSs.InserterSwingTime.L(DataUtils.FormatAmount(inserter.inserterSwingTime, UnitOfMeasure.Second)); break; case EntityBeacon beacon: - miscText = "Beacon efficiency: " + DataUtils.FormatAmount(beacon.BeaconEfficiency(quality), UnitOfMeasure.Percent); + miscText = LSs.BeaconEfficiency.L(DataUtils.FormatAmount(beacon.BeaconEfficiency(quality), UnitOfMeasure.Percent)); break; case EntityAccumulator accumulator: - miscText = "Accumulator charge: " + DataUtils.FormatAmount(accumulator.AccumulatorCapacity(quality), UnitOfMeasure.Megajoule); + miscText = LSs.AccumulatorCapacity.L(DataUtils.FormatAmount(accumulator.AccumulatorCapacity(quality), UnitOfMeasure.Megajoule)); break; case EntityAttractor attractor: if (attractor.baseCraftingSpeed > 0f) { - miscText = "Power production (average usable): " + DataUtils.FormatAmount(attractor.CraftingSpeed(quality), UnitOfMeasure.Megawatt); - miscText += $"\n Build in a {attractor.ConstructionGrid(quality)}-tile square grid"; - miscText += "\nProtection range: " + DataUtils.FormatAmount(attractor.Range(quality), UnitOfMeasure.None); - miscText += "\nCollection efficiency: " + DataUtils.FormatAmount(attractor.Efficiency(quality), UnitOfMeasure.Percent); + miscText = LSs.LightningAttractorExtraInfo.L(DataUtils.FormatAmount(attractor.CraftingSpeed(quality), UnitOfMeasure.Megawatt), attractor.ConstructionGrid(quality), DataUtils.FormatAmount(attractor.Range(quality), UnitOfMeasure.None), DataUtils.FormatAmount(attractor.Efficiency(quality), UnitOfMeasure.Percent)); } break; case EntityCrafter solarPanel: if (solarPanel.baseCraftingSpeed > 0f && entity.factorioType == "solar-panel") { - miscText = "Power production (average): " + DataUtils.FormatAmount(solarPanel.CraftingSpeed(quality), UnitOfMeasure.Megawatt); + miscText = LSs.SolarPanelAverageProduction.L(DataUtils.FormatAmount(solarPanel.CraftingSpeed(quality), UnitOfMeasure.Megawatt)); } break; } @@ -366,12 +365,12 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { private void BuildGoods(Goods goods, Quality quality, ImGui gui) { if (goods.showInExplorers) { using (gui.EnterGroup(contentPadding)) { - gui.BuildText("Middle mouse button to open Never Enough Items Explorer for this " + goods.type, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.OpenNeieMiddleClickHint.L(goods.type), TextBlockDisplayStyle.WrappedText); } } if (goods.production.Length > 0) { - BuildSubHeader(gui, "Made with"); + BuildSubHeader(gui, LSs.TooltipHeaderProductionRecipes); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, goods.production, 2); if (tooltipOptions.HintLocations.HasFlag(HintLocations.OnProducingRecipes)) { @@ -382,14 +381,14 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { } if (goods.miscSources.Length > 0) { - BuildSubHeader(gui, "Sources"); + BuildSubHeader(gui, LSs.TooltipHeaderMiscellaneousSources); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, goods.miscSources, 2); } } if (goods.usages.Length > 0) { - BuildSubHeader(gui, "Needed for"); + BuildSubHeader(gui, LSs.TooltipHeaderConsumptionRecipes); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, goods.usages, 4); if (tooltipOptions.HintLocations.HasFlag(HintLocations.OnConsumingRecipes)) { @@ -400,10 +399,10 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { } if (goods is Item { spoilResult: FactorioObject spoiled } perishable) { - BuildSubHeader(gui, "Perishable"); + BuildSubHeader(gui, LSs.Perishable); using (gui.EnterGroup(contentPadding)) { float spoilTime = perishable.GetSpoilTime(quality) / Project.current.settings.spoilingRate; - gui.BuildText($"After {DataUtils.FormatTime(spoilTime)}, spoils into"); + gui.BuildText(LSs.ItemSpoils.L(DataUtils.FormatTime(spoilTime))); gui.BuildFactorioObjectButtonWithText(spoiled, iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); tooltipOptions.ExtraSpoilInformation?.Invoke(gui); } @@ -411,10 +410,10 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { if (goods.fuelFor.Length > 0) { if (goods.fuelValue > 0f) { - BuildSubHeader(gui, "Fuel value " + DataUtils.FormatAmount(goods.fuelValue, UnitOfMeasure.Megajoule) + " used for:"); + BuildSubHeader(gui, LSs.FuelValueCanBeUsed.L(DataUtils.FormatAmount(goods.fuelValue, UnitOfMeasure.Megajoule))); } else { - BuildSubHeader(gui, "Can be used as fuel for:"); + BuildSubHeader(gui, LSs.FuelValueZeroCanBeUsed); } using (gui.EnterGroup(contentPadding)) { @@ -430,40 +429,40 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { } if (item.placeResult != null) { - BuildSubHeader(gui, "Place result"); + BuildSubHeader(gui, LSs.TooltipHeaderItemPlacementResult); using (gui.EnterGroup(contentPadding)) { BuildItem(gui, item.placeResult); } } if (item is Module { moduleSpecification: ModuleSpecification moduleSpecification }) { - BuildSubHeader(gui, "Module parameters"); + BuildSubHeader(gui, LSs.TooltipHeaderModuleProperties); using (gui.EnterGroup(contentPadding)) { if (moduleSpecification.baseProductivity != 0f) { - gui.BuildText("Productivity: " + DataUtils.FormatAmount(moduleSpecification.Productivity(quality), UnitOfMeasure.Percent)); + gui.BuildText(LSs.ProductivityProperty.L(DataUtils.FormatAmount(moduleSpecification.Productivity(quality), UnitOfMeasure.Percent))); } if (moduleSpecification.baseSpeed != 0f) { - gui.BuildText("Speed: " + DataUtils.FormatAmount(moduleSpecification.Speed(quality), UnitOfMeasure.Percent)); + gui.BuildText(LSs.SpeedProperty.L(DataUtils.FormatAmount(moduleSpecification.Speed(quality), UnitOfMeasure.Percent))); } if (moduleSpecification.baseConsumption != 0f) { - gui.BuildText("Consumption: " + DataUtils.FormatAmount(moduleSpecification.Consumption(quality), UnitOfMeasure.Percent)); + gui.BuildText(LSs.ConsumptionProperty.L(DataUtils.FormatAmount(moduleSpecification.Consumption(quality), UnitOfMeasure.Percent))); } if (moduleSpecification.basePollution != 0f) { - gui.BuildText("Pollution: " + DataUtils.FormatAmount(moduleSpecification.Pollution(quality), UnitOfMeasure.Percent)); + gui.BuildText(LSs.PollutionProperty.L(DataUtils.FormatAmount(moduleSpecification.Pollution(quality), UnitOfMeasure.Percent))); } if (moduleSpecification.baseQuality != 0f) { - gui.BuildText("Quality: " + DataUtils.FormatAmount(moduleSpecification.Quality(quality), UnitOfMeasure.Percent)); + gui.BuildText(LSs.QualityProperty.L(DataUtils.FormatAmount(moduleSpecification.Quality(quality), UnitOfMeasure.Percent))); } } } using (gui.EnterGroup(contentPadding)) { - gui.BuildText("Stack size: " + item.stackSize); - gui.BuildText("Rocket capacity: " + DataUtils.FormatAmount(item.rocketCapacity, UnitOfMeasure.None)); + gui.BuildText(LSs.ItemStackSize.L(item.stackSize)); + gui.BuildText(LSs.ItemRocketCapacity.L(DataUtils.FormatAmount(item.rocketCapacity, UnitOfMeasure.None))); } } } @@ -488,43 +487,42 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { float waste = rec.RecipeWaste(); if (waste > 0.01f) { int wasteAmount = MathUtils.Round(waste * 100f); - string wasteText = ". (Wasting " + wasteAmount + "% of YAFC cost)"; TextBlockDisplayStyle style = TextBlockDisplayStyle.WrappedText with { Color = wasteAmount < 90 ? SchemeColor.BackgroundText : SchemeColor.Error }; if (recipe.products.Length == 1) { - gui.BuildText("YAFC analysis: There are better recipes to create " + recipe.products[0].goods.locName + wasteText, style); + gui.BuildText(LSs.AnalysisBetterRecipesToCreate.L(recipe.products[0].goods.locName, wasteAmount), style); } else if (recipe.products.Length > 0) { - gui.BuildText("YAFC analysis: There are better recipes to create each of the products" + wasteText, style); + gui.BuildText(LSs.AnalysisBetterRecipesToCreateAll.L(wasteAmount), style); } else { - gui.BuildText("YAFC analysis: This recipe wastes useful products. Don't do this recipe.", style); + gui.BuildText(LSs.AnalysisWastesUsefulProducts, style); } } } if (recipe.flags.HasFlags(RecipeFlags.UsesFluidTemperature)) { - gui.BuildText("Uses fluid temperature"); + gui.BuildText(LSs.RecipeUsesFluidTemperature); } if (recipe.flags.HasFlags(RecipeFlags.UsesMiningProductivity)) { - gui.BuildText("Uses mining productivity"); + gui.BuildText(LSs.RecipeUsesMiningProductivity); } if (recipe.flags.HasFlags(RecipeFlags.ScaleProductionWithPower)) { - gui.BuildText("Production scaled with power"); + gui.BuildText(LSs.RecipeProductionScalesWithPower); } } if (recipe is Recipe { products.Length: > 0 } && !(recipe.products.Length == 1 && recipe.products[0].IsSimple)) { - BuildSubHeader(gui, "Products"); + BuildSubHeader(gui, LSs.TooltipHeaderRecipeProducts); using (gui.EnterGroup(contentPadding)) { - string? extraText = recipe is Recipe { preserveProducts: true } ? ", preserved until removed from the machine" : null; + string? extraText = recipe is Recipe { preserveProducts: true } ? LSs.ProductSuffixPreserved : null; foreach (var product in recipe.products) { BuildItem(gui, product, extraText); } } } - BuildSubHeader(gui, "Made in"); + BuildSubHeader(gui, LSs.TooltipHeaderRecipeCrafters); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, recipe.crafters, 2); } @@ -532,7 +530,7 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { List allowedModules = [.. Database.allModules.Where(recipe.CanAcceptModule)]; if (allowedModules.Count > 0) { - BuildSubHeader(gui, "Allowed modules"); + BuildSubHeader(gui, LSs.TooltipHeaderAllowedModules); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, allowedModules, 1); } @@ -556,7 +554,7 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { } if (recipe is Recipe lockedRecipe && !lockedRecipe.enabled) { - BuildSubHeader(gui, "Unlocked by"); + BuildSubHeader(gui, LSs.TooltipHeaderUnlockedByTechnologies); using (gui.EnterGroup(contentPadding)) { if (lockedRecipe.technologyUnlock.Length > 2) { BuildIconRow(gui, lockedRecipe.technologyUnlock, 1); @@ -594,19 +592,19 @@ private static void BuildTechnology(Technology technology, ImGui gui) { if (!technology.enabled) { using (gui.EnterGroup(contentPadding)) { - gui.BuildText("This technology is disabled and cannot be researched.", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.TechnologyIsDisabled, TextBlockDisplayStyle.WrappedText); } } if (technology.prerequisites.Length > 0) { - BuildSubHeader(gui, "Prerequisites"); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyPrerequisites); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.prerequisites, 1); } } if (isResearchTriggerCraft) { - BuildSubHeader(gui, "Item crafting required"); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyItemCrafting); using (gui.EnterGroup(contentPadding)) { using var grid = gui.EnterInlineGrid(3f); grid.Next(); @@ -614,46 +612,46 @@ private static void BuildTechnology(Technology technology, ImGui gui) { } } else if (isResearchTriggerCapture) { - BuildSubHeader(gui, technology.triggerEntities.Count == 1 ? "Capture this entity" : "Capture any entity"); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyCapture.L(technology.triggerEntities.Count)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.triggerEntities, 2); } } else if (isResearchTriggerMine) { - BuildSubHeader(gui, technology.triggerEntities.Count == 1 ? "Mine this entity" : "Mine any entity"); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyMineEntity.L(technology.triggerEntities.Count)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.triggerEntities, 2); } } else if (isResearchTriggerBuild) { - BuildSubHeader(gui, technology.triggerEntities.Count == 1 ? "Build this entity" : "Build any entity"); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyBuildEntity.L(technology.triggerEntities.Count)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.triggerEntities, 2); } } else if (isResearchTriggerPlatform) { List items = [.. Database.items.all.Where(i => i.factorioType == "space-platform-starter-pack")]; - BuildSubHeader(gui, items.Count == 1 ? "Launch this item" : "Launch any item"); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyLaunchItem.L(items.Count)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, items, 2); } } else if (isResearchTriggerLaunch) { - BuildSubHeader(gui, "Launch this item"); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyLaunchItem.L(1)); using (gui.EnterGroup(contentPadding)) { gui.BuildFactorioObjectButtonWithText(technology.triggerItem); } } if (technology.unlockRecipes.Count > 0) { - BuildSubHeader(gui, "Unlocks recipes"); + BuildSubHeader(gui, LSs.TooltipHeaderUnlocksRecipes); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.unlockRecipes, 2); } } if (technology.unlockLocations.Count > 0) { - BuildSubHeader(gui, "Unlocks locations"); + BuildSubHeader(gui, LSs.TooltipHeaderUnlocksLocations); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.unlockLocations, 2); } @@ -661,7 +659,7 @@ private static void BuildTechnology(Technology technology, ImGui gui) { var packs = TechnologyScienceAnalysis.Instance.allSciencePacks[technology]; if (packs.Length > 0) { - BuildSubHeader(gui, "Total science required"); + BuildSubHeader(gui, LSs.TooltipHeaderTotalScienceRequired); using (gui.EnterGroup(contentPadding)) { using var grid = gui.EnterInlineGrid(3f); foreach (var pack in packs) { @@ -675,25 +673,25 @@ private static void BuildTechnology(Technology technology, ImGui gui) { private static void BuildQuality(Quality quality, ImGui gui) { using (gui.EnterGroup(contentPadding)) { if (quality.UpgradeChance > 0) { - gui.BuildText("Upgrade chance: " + DataUtils.FormatAmount(quality.UpgradeChance, UnitOfMeasure.Percent) + " (multiplied by module bonus)"); + gui.BuildText(LSs.TooltipQualityUpgradeChance.L(DataUtils.FormatAmount(quality.UpgradeChance, UnitOfMeasure.Percent))); } } - BuildSubHeader(gui, "Quality bonuses"); + BuildSubHeader(gui, LSs.TooltipHeaderQualityBonuses); using (gui.EnterGroup(contentPadding)) { if (quality == Quality.Normal) { - gui.BuildText("Normal quality provides no bonuses.", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.TooltipNoNormalBonuses, TextBlockDisplayStyle.WrappedText); return; } gui.allocator = RectAllocator.LeftAlign; (string left, string right)[] text = [ - ("Crafting speed:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent)), - ("Accumulator capacity:", '+' + DataUtils.FormatAmount(quality.AccumulatorCapacityBonus, UnitOfMeasure.Percent)), - ("Module effects:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent) + '*'), - ("Beacon transmission efficiency:", '+' + DataUtils.FormatAmount(quality.BeaconTransmissionBonus, UnitOfMeasure.None)), - ("Time before spoiling:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent)), - ("Lightning attractor range & efficiency:", '+' + DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent)), + (LSs.TooltipQualityCraftingSpeed, LSs.QualityBonusValue.L(DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent))), + (LSs.TooltipQualityAccumulatorCapacity, LSs.QualityBonusValue.L(DataUtils.FormatAmount(quality.AccumulatorCapacityBonus, UnitOfMeasure.Percent))), + (LSs.TooltipQualityModuleEffects, LSs.QualityBonusValueWithFootnote.L(DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent))), + (LSs.TooltipQualityBeaconTransmission, LSs.QualityBonusValue.L(DataUtils.FormatAmount(quality.BeaconTransmissionBonus, UnitOfMeasure.None))), + (LSs.TooltipQualityTimeBeforeSpoiling, LSs.QualityBonusValue.L(DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent))), + (LSs.TooltipQualityLightningAttractor, LSs.QualityBonusValue.L(DataUtils.FormatAmount(quality.StandardBonus, UnitOfMeasure.Percent))), ]; float rightWidth = text.Max(t => gui.GetTextDimensions(out _, t.right).X); @@ -704,7 +702,7 @@ private static void BuildQuality(Quality quality, ImGui gui) { Rect rect = new(gui.statePosition.Width - rightWidth, gui.lastRect.Y, rightWidth, gui.lastRect.Height); gui.DrawText(rect, right); } - gui.BuildText("* Only applied to beneficial module effects.", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.TooltipQualityModuleFootnote, TextBlockDisplayStyle.WrappedText); } } diff --git a/Yafc/Windows/AboutScreen.cs b/Yafc/Windows/AboutScreen.cs index 73f89fa4..b9e7311c 100644 --- a/Yafc/Windows/AboutScreen.cs +++ b/Yafc/Windows/AboutScreen.cs @@ -1,84 +1,81 @@ -using Yafc.UI; +using Yafc.I18n; +using Yafc.UI; namespace Yafc; public class AboutScreen : WindowUtility { public const string Github = "https://github.com/have-fun-was-taken/yafc-ce"; - public AboutScreen(Window parent) : base(ImGuiUtils.DefaultScreenPadding) => Create("About YAFC-CE", 50, parent); + public AboutScreen(Window parent) : base(ImGuiUtils.DefaultScreenPadding) => Create(LSs.AboutYafc, 50, parent); protected override void BuildContents(ImGui gui) { gui.allocator = RectAllocator.Center; - gui.BuildText("Yet Another Factorio Calculator", new TextBlockDisplayStyle(Font.header, Alignment: RectAlignment.Middle)); - gui.BuildText("(Community Edition)", TextBlockDisplayStyle.Centered); - gui.BuildText("Copyright 2020-2021 ShadowTheAge", TextBlockDisplayStyle.Centered); - gui.BuildText("Copyright 2024 YAFC Community", TextBlockDisplayStyle.Centered); + gui.BuildText(LSs.FullName, new TextBlockDisplayStyle(Font.header, Alignment: RectAlignment.Middle)); + gui.BuildText(LSs.AboutCommunityEdition, TextBlockDisplayStyle.Centered); + gui.BuildText(LSs.AboutCopyrightShadow, TextBlockDisplayStyle.Centered); + gui.BuildText(LSs.AboutCopyrightCommunity, TextBlockDisplayStyle.Centered); gui.allocator = RectAllocator.LeftAlign; gui.AllocateSpacing(1.5f); - string gnuMessage = "This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public " + - "License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version."; - gui.BuildText(gnuMessage, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.AboutCopyleftGpl3, TextBlockDisplayStyle.WrappedText); - string noWarrantyMessage = "This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the " + - "implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details."; - gui.BuildText(noWarrantyMessage, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.AboutWarrantyDisclaimer, TextBlockDisplayStyle.WrappedText); using (gui.EnterRow(0.3f)) { - gui.BuildText("Full license text:"); + gui.BuildText(LSs.AboutFullLicenseText); BuildLink(gui, "https://gnu.org/licenses/gpl-3.0.html"); } using (gui.EnterRow(0.3f)) { - gui.BuildText("Github YAFC-CE page and documentation:"); + gui.BuildText(LSs.AboutGithubPage); BuildLink(gui, Github); } gui.AllocateSpacing(1.5f); - gui.BuildText("Free and open-source third-party libraries used:", Font.subheader); - BuildLink(gui, "https://dotnet.microsoft.com/", "Microsoft .NET core and libraries"); + gui.BuildText(LSs.AboutLibraries, Font.subheader); + BuildLink(gui, "https://dotnet.microsoft.com/", LSs.AboutDotNetCore); using (gui.EnterRow(0.3f)) { BuildLink(gui, "https://libsdl.org/index.php", "Simple DirectMedia Layer 2.0"); - gui.BuildText("and"); + gui.BuildText(LSs.AboutAnd); BuildLink(gui, "https://github.com/flibitijibibo/SDL2-CS", "SDL2-CS"); } using (gui.EnterRow(0.3f)) { - gui.BuildText("Libraries for SDL2:"); + gui.BuildText(LSs.AboutSdl2Libraries); BuildLink(gui, "http://libpng.org/pub/png/libpng.html", "libpng,"); BuildLink(gui, "http://libjpeg.sourceforge.net/", "libjpeg,"); BuildLink(gui, "https://freetype.org", "libfreetype"); - gui.BuildText("and"); + gui.BuildText(LSs.AboutAnd); BuildLink(gui, "https://zlib.net/", "zlib"); } using (gui.EnterRow(0.3f)) { gui.BuildText("Google"); BuildLink(gui, "https://developers.google.com/optimization", "OR-Tools,"); - BuildLink(gui, "https://fonts.google.com/specimen/Roboto", "Roboto font family"); - BuildLink(gui, "https://fonts.google.com/noto", "Noto Sans font family"); - gui.BuildText("and"); - BuildLink(gui, "https://material.io/resources/icons", "Material Design Icon collection"); + BuildLink(gui, "https://fonts.google.com/specimen/Roboto", LSs.AboutRobotoFontFamily); + BuildLink(gui, "https://fonts.google.com/noto", LSs.AboutNotoSansFamily); + gui.BuildText(LSs.AboutAnd); + BuildLink(gui, "https://material.io/resources/icons", LSs.AboutMaterialDesignIcon); } using (gui.EnterRow(0.3f)) { BuildLink(gui, "https://lua.org/", "Lua 5.2"); - gui.BuildText("plus"); - BuildLink(gui, "https://github.com/pkulchenko/serpent", "Serpent library"); - gui.BuildText("and small bits from"); + gui.BuildText(LSs.AboutPlus); + BuildLink(gui, "https://github.com/pkulchenko/serpent", LSs.AboutSerpentLibrary); + gui.BuildText(LSs.AboutAndSmallBits); BuildLink(gui, "https://github.com/NLua", "NLua"); } using (gui.EnterRow(0.3f)) { - BuildLink(gui, "https://wiki.factorio.com/", "Documentation on Factorio Wiki"); - gui.BuildText("and"); - BuildLink(gui, "https://lua-api.factorio.com/latest/", "Factorio API reference"); + BuildLink(gui, "https://wiki.factorio.com/", LSs.AboutFactorioWiki); + gui.BuildText(LSs.AboutAnd); + BuildLink(gui, "https://lua-api.factorio.com/latest/", LSs.AboutFactorioLuaApi); } gui.AllocateSpacing(1.5f); gui.allocator = RectAllocator.Center; - gui.BuildText("Factorio name, content and materials are trademarks and copyrights of Wube Software"); + gui.BuildText(LSs.AboutFactorioTrademarkDisclaimer); BuildLink(gui, "https://factorio.com/"); } diff --git a/Yafc/Windows/DependencyExplorer.cs b/Yafc/Windows/DependencyExplorer.cs index 9b87f5b0..5f55ee51 100644 --- a/Yafc/Windows/DependencyExplorer.cs +++ b/Yafc/Windows/DependencyExplorer.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using SDL2; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -16,17 +17,17 @@ public class DependencyExplorer : PseudoScreen { private static readonly Dictionary dependencyListTexts = new Dictionary() { - {DependencyNode.Flags.Fuel, ("Fuel", "There is no fuel to power this entity")}, - {DependencyNode.Flags.Ingredient, ("Ingredient", "There are no ingredients to this recipe")}, - {DependencyNode.Flags.IngredientVariant, ("Ingredient", "There are no ingredient variants for this recipe")}, - {DependencyNode.Flags.CraftingEntity, ("Crafter", "There are no crafters that can craft this item")}, - {DependencyNode.Flags.Source, ("Source", "This item have no sources")}, - {DependencyNode.Flags.TechnologyUnlock, ("Research", "This recipe is disabled and there are no technologies to unlock it")}, - {DependencyNode.Flags.TechnologyPrerequisites, ("Research", "There are no technology prerequisites")}, - {DependencyNode.Flags.ItemToPlace, ("Item", "This entity cannot be placed")}, - {DependencyNode.Flags.SourceEntity, ("Source", "This recipe requires another entity")}, - {DependencyNode.Flags.Disabled, ("", "This technology is disabled")}, - {DependencyNode.Flags.Location, ("Location", "There are no locations that spawn this entity")}, + {DependencyNode.Flags.Fuel, (LSs.DependencyFuel, LSs.DependencyFuelMissing)}, + {DependencyNode.Flags.Ingredient, (LSs.DependencyIngredient, LSs.DependencyIngredientMissing)}, + {DependencyNode.Flags.IngredientVariant, (LSs.DependencyIngredient, LSs.DependencyIngredientVariantsMissing)}, + {DependencyNode.Flags.CraftingEntity, (LSs.DependencyCrafter, LSs.DependencyCrafterMissing)}, + {DependencyNode.Flags.Source, (LSs.DependencySource, LSs.DependencySourcesMissing)}, + {DependencyNode.Flags.TechnologyUnlock, (LSs.DependencyTechnology, LSs.DependencyTechnologyMissing)}, + {DependencyNode.Flags.TechnologyPrerequisites, (LSs.DependencyTechnology, LSs.DependencyTechnologyNoPrerequisites)}, + {DependencyNode.Flags.ItemToPlace, (LSs.DependencyItem, LSs.DependencyItemMissing)}, + {DependencyNode.Flags.SourceEntity, (LSs.DependencySource, LSs.DependencyMapSourceMissing)}, + {DependencyNode.Flags.Disabled, ("", LSs.DependencyTechnologyDisabled)}, + {DependencyNode.Flags.Location, (LSs.DependencyLocation, LSs.DependencyLocationMissing)}, }; public DependencyExplorer(FactorioObject current) : base(60f) { @@ -41,7 +42,7 @@ private void DrawFactorioObject(ImGui gui, FactorioId id) { FactorioObject obj = Database.objects[id]; using (gui.EnterGroup(listPad, RectAllocator.LeftRow)) { gui.BuildFactorioObjectIcon(obj); - string text = obj.locName + " (" + obj.type + ")"; + string text = LSs.NameWithType.L(obj.locName, obj.type); gui.RemainingRow(0.5f).BuildText(text, TextBlockDisplayStyle.WrappedText with { Color = obj.IsAccessible() ? SchemeColor.BackgroundText : SchemeColor.BackgroundTextFaint }); } if (gui.BuildFactorioObjectButtonBackground(gui.lastRect, obj, tooltipOptions: new() { ShowTypeInHeader = true }) == Click.Left) { @@ -60,13 +61,13 @@ private void DrawDependencies(ImGui gui) { if (elements.Count > 0) { gui.AllocateSpacing(0.5f); if (elements.Count == 1) { - gui.BuildText("Require this " + dependencyType.name + ":"); + gui.BuildText(LSs.DependencyRequireSingle.L(dependencyType.name)); } else if (flags.HasFlags(DependencyNode.Flags.RequireEverything)) { - gui.BuildText("Require ALL of these " + dependencyType.name + "s:"); + gui.BuildText(LSs.DependencyRequireAll.L(dependencyType.name)); } else { - gui.BuildText("Require ANY of these " + dependencyType.name + "s:"); + gui.BuildText(LSs.DependencyRequireAny.L(dependencyType.name)); } gui.AllocateSpacing(0.5f); @@ -77,10 +78,10 @@ private void DrawDependencies(ImGui gui) { else { string text = dependencyType.missingText; if (Database.rootAccessible.Contains(current)) { - text += ", but it is inherently accessible."; + text = LSs.DependencyAccessibleAnyway.L(text); } else { - text += ", and it is inaccessible."; + text = LSs.DependencyAndNotAccessible.L(text); } gui.BuildText(text, TextBlockDisplayStyle.WrappedText); @@ -105,39 +106,39 @@ private void SetFlag(ProjectPerItemFlags flag, bool set) { public override void Build(ImGui gui) { gui.allocator = RectAllocator.Center; - BuildHeader(gui, "Dependency explorer"); + BuildHeader(gui, LSs.DependencyExplorer); using (gui.EnterRow()) { - gui.BuildText("Currently inspecting:", Font.subheader); + gui.BuildText(LSs.DependencyCurrentlyInspecting, Font.subheader); if (gui.BuildFactorioObjectButtonWithText(current) == Click.Left) { - SelectSingleObjectPanel.Select(Database.objects.explorable, "Select something", Change); + SelectSingleObjectPanel.Select(Database.objects.explorable, LSs.DependencySelectSomething, Change); } - gui.DrawText(gui.lastRect, "(Click to change)", RectAlignment.MiddleRight, color: TextBlockDisplayStyle.HintText.Color); + gui.DrawText(gui.lastRect, LSs.DependencyClickToChangeHint, RectAlignment.MiddleRight, color: TextBlockDisplayStyle.HintText.Color); } using (gui.EnterRow()) { var settings = Project.current.settings; if (current.IsAccessible()) { if (current.IsAutomatable()) { - gui.BuildText("Status: Automatable"); + gui.BuildText(LSs.DependencyAutomatable); } else { - gui.BuildText("Status: Accessible, Not automatable"); + gui.BuildText(LSs.DependencyAccessible); } if (settings.Flags(current).HasFlags(ProjectPerItemFlags.MarkedAccessible)) { - gui.BuildText("Manually marked as accessible."); - if (gui.BuildLink("Clear mark")) { + gui.BuildText(LSs.DependencyMarkedAccessible); + if (gui.BuildLink(LSs.DependencyClearMark)) { SetFlag(ProjectPerItemFlags.MarkedAccessible, false); NeverEnoughItemsPanel.Refresh(); } } else { - if (gui.BuildLink("Mark as inaccessible")) { + if (gui.BuildLink(LSs.DependencyMarkNotAccessible)) { SetFlag(ProjectPerItemFlags.MarkedInaccessible, true); NeverEnoughItemsPanel.Refresh(); } - if (gui.BuildLink("Mark as accessible without milestones")) { + if (gui.BuildLink(LSs.DependencyMarkAccessibleIgnoringMilestones)) { SetFlag(ProjectPerItemFlags.MarkedAccessible, true); NeverEnoughItemsPanel.Refresh(); } @@ -145,15 +146,15 @@ public override void Build(ImGui gui) { } else { if (settings.Flags(current).HasFlags(ProjectPerItemFlags.MarkedInaccessible)) { - gui.BuildText("Status: Marked as inaccessible"); - if (gui.BuildLink("Clear mark")) { + gui.BuildText(LSs.DependencyMarkedNotAccessible); + if (gui.BuildLink(LSs.DependencyClearMark)) { SetFlag(ProjectPerItemFlags.MarkedInaccessible, false); NeverEnoughItemsPanel.Refresh(); } } else { - gui.BuildText("Status: Not accessible. Wrong?"); - if (gui.BuildLink("Manually mark as accessible")) { + gui.BuildText(LSs.DependencyNotAccessible); + if (gui.BuildLink(LSs.DependencyMarkAccessible)) { SetFlag(ProjectPerItemFlags.MarkedAccessible, true); NeverEnoughItemsPanel.Refresh(); } @@ -163,10 +164,10 @@ public override void Build(ImGui gui) { gui.AllocateSpacing(2f); using var split = gui.EnterHorizontalSplit(2); split.Next(); - gui.BuildText("Dependencies:", Font.subheader); + gui.BuildText(LSs.DependencyHeaderDependencies, Font.subheader); dependencies.Build(gui); split.Next(); - gui.BuildText("Dependents:", Font.subheader); + gui.BuildText(LSs.DependencyHeaderDependents, Font.subheader); dependents.Build(gui); } diff --git a/Yafc/Windows/ErrorListPanel.cs b/Yafc/Windows/ErrorListPanel.cs index dea80745..7ad7aa3f 100644 --- a/Yafc/Windows/ErrorListPanel.cs +++ b/Yafc/Windows/ErrorListPanel.cs @@ -1,4 +1,5 @@ -using Yafc.Model; +using Yafc.I18n; +using Yafc.Model; using Yafc.UI; namespace Yafc; @@ -23,13 +24,13 @@ private void BuildErrorList(ImGui gui) { public static void Show(ErrorCollector collector) => _ = MainScreen.Instance.ShowPseudoScreen(new ErrorListPanel(collector)); public override void Build(ImGui gui) { if (collector.severity == ErrorSeverity.Critical) { - BuildHeader(gui, "Loading failed"); + BuildHeader(gui, LSs.ErrorLoadingFailed); } else if (collector.severity >= ErrorSeverity.MinorDataLoss) { - BuildHeader(gui, "Loading completed with errors"); + BuildHeader(gui, LSs.ErrorButLoadingSucceeded); } else { - BuildHeader(gui, "Analysis warnings"); + BuildHeader(gui, LSs.AnalysisWarnings); } verticalList.Build(gui); diff --git a/Yafc/Windows/FilesystemScreen.cs b/Yafc/Windows/FilesystemScreen.cs index b2adcc1b..7909f74e 100644 --- a/Yafc/Windows/FilesystemScreen.cs +++ b/Yafc/Windows/FilesystemScreen.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using SDL2; +using Yafc.I18n; using Yafc.UI; namespace Yafc; @@ -135,7 +136,7 @@ public void UpdatePossibleResult() { EntryType.Directory => (Icon.Folder, Path.GetFileName(data.location)), EntryType.Drive => (Icon.FolderOpen, data.location), EntryType.ParentDirectory => (Icon.Upload, ".."), - EntryType.CreateDirectory => (Icon.NewFolder, "Create directory here"), + EntryType.CreateDirectory => (Icon.NewFolder, LSs.BrowserCreateDirectory), _ => (Icon.Settings, Path.GetFileName(data.location)), }; diff --git a/Yafc/Windows/ImageSharePanel.cs b/Yafc/Windows/ImageSharePanel.cs index 77b176cc..8911e866 100644 --- a/Yafc/Windows/ImageSharePanel.cs +++ b/Yafc/Windows/ImageSharePanel.cs @@ -1,6 +1,7 @@ using System.IO; using System.Runtime.InteropServices; using SDL2; +using Yafc.I18n; using Yafc.UI; namespace Yafc; @@ -24,19 +25,19 @@ public ImageSharePanel(MemoryDrawingSurface surface, string name) { } public override void Build(ImGui gui) { - BuildHeader(gui, "Image generated"); + BuildHeader(gui, LSs.SharingImageGenerated); gui.BuildText(header, TextBlockDisplayStyle.WrappedText); - if (gui.BuildButton("Save as PNG")) { + if (gui.BuildButton(LSs.SaveAsPng)) { SaveAsPng(); } - if (gui.BuildButton("Save to temp folder and open")) { + if (gui.BuildButton(LSs.SaveAndOpen)) { surface.SavePng(TempImageFile); Ui.VisitLink("file:///" + TempImageFile); } if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - && gui.BuildButton(copied ? "Copied to clipboard" : "Copy to clipboard (Ctrl+" + ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_C) + ")", active: !copied)) { + && gui.BuildButton(copied ? LSs.CopiedToClipboard : LSs.CopyToClipboardWithShortcut.L(ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_C)), active: !copied)) { WindowsClipboard.CopySurfaceToClipboard(surface); copied = true; @@ -53,7 +54,7 @@ public override bool KeyDown(SDL.SDL_Keysym key) { } private async void SaveAsPng() { - string? path = await new FilesystemScreen(header, "Save as PNG", "Save", null, FilesystemScreen.Mode.SelectOrCreateFile, name + ".png", MainScreen.Instance, null, "png"); + string? path = await new FilesystemScreen(header, LSs.SaveAsPng, LSs.Save, null, FilesystemScreen.Mode.SelectOrCreateFile, name + ".png", MainScreen.Instance, null, "png"); if (path != null) { surface?.SavePng(path); } diff --git a/Yafc/Windows/MainScreen.PageListSearch.cs b/Yafc/Windows/MainScreen.PageListSearch.cs index 37dbb2e8..c4ba47eb 100644 --- a/Yafc/Windows/MainScreen.PageListSearch.cs +++ b/Yafc/Windows/MainScreen.PageListSearch.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -45,16 +46,16 @@ public void Build(ImGui gui, Action updatePageList) { } gui.SetTextInputFocus(gui.lastContentRect, query.query); - gui.BuildText("Search in:"); + gui.BuildText(LSs.SearchAllHeader); using (gui.EnterRow()) { - buildCheckbox(gui, "Page name", ref checkboxValues[(int)PageSearchOption.PageName]); - buildCheckbox(gui, "Desired products", ref checkboxValues[(int)PageSearchOption.DesiredProducts]); - buildCheckbox(gui, "Recipes", ref checkboxValues[(int)PageSearchOption.Recipes]); + buildCheckbox(gui, LSs.SearchAllLocationPageName, ref checkboxValues[(int)PageSearchOption.PageName]); + buildCheckbox(gui, LSs.SearchAllLocationOutputs, ref checkboxValues[(int)PageSearchOption.DesiredProducts]); + buildCheckbox(gui, LSs.SearchAllLocationRecipes, ref checkboxValues[(int)PageSearchOption.Recipes]); } using (gui.EnterRow()) { - buildCheckbox(gui, "Ingredients", ref checkboxValues[(int)PageSearchOption.Ingredients]); - buildCheckbox(gui, "Extra products", ref checkboxValues[(int)PageSearchOption.ExtraProducts]); - if (gui.BuildCheckBox("All", checkboxValues.All(x => x), out bool checkAll)) { + buildCheckbox(gui, LSs.SearchAllLocationInputs, ref checkboxValues[(int)PageSearchOption.Ingredients]); + buildCheckbox(gui, LSs.SearchAllLocationExtraOutputs, ref checkboxValues[(int)PageSearchOption.ExtraProducts]); + if (gui.BuildCheckBox(LSs.SearchAllLocationAll, checkboxValues.All(x => x), out bool checkAll)) { if (checkAll) { // Save the previous state, so we can restore it if necessary. Array.Copy(checkboxValues, previousCheckboxValues, (int)PageSearchOption.MustBeLastValue); @@ -68,9 +69,9 @@ public void Build(ImGui gui, Action updatePageList) { } } using (gui.EnterRow()) { - buildRadioButton(gui, "Localized names", SearchNameMode.Localized); - buildRadioButton(gui, "Internal names", SearchNameMode.Internal); - buildRadioButton(gui, "Both", SearchNameMode.Both); + buildRadioButton(gui, LSs.SearchAllLocalizedStrings, SearchNameMode.Localized); + buildRadioButton(gui, LSs.SearchAllInternalStrings, SearchNameMode.Internal); + buildRadioButton(gui, LSs.SearchAllBothStrings, SearchNameMode.Both); } } diff --git a/Yafc/Windows/MainScreen.cs b/Yafc/Windows/MainScreen.cs index 64d28c0c..4bff3878 100644 --- a/Yafc/Windows/MainScreen.cs +++ b/Yafc/Windows/MainScreen.cs @@ -3,13 +3,13 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Net.Http; using System.Numerics; using System.Text.Json; using System.Threading.Tasks; using SDL2; using Serilog; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -57,7 +57,7 @@ public MainScreen(int display, Project project) : base(default, Preferences.Inst tabBar = new MainScreenTabBar(this); allPages = new VirtualScrollList(30, new Vector2(float.PositiveInfinity, 2f), BuildPage, collapsible: true); - Create("Yet Another Factorio Calculator CE v" + YafcLib.version.ToString(3), display, Preferences.Instance.initialMainScreenWidth, + Create(LSs.FullNameWithVersion.L(YafcLib.version.ToString(3)), display, Preferences.Instance.initialMainScreenWidth, Preferences.Instance.initialMainScreenHeight, Preferences.Instance.maximizeMainScreen); SetProject(project); @@ -249,15 +249,13 @@ private void BuildTabBar(ImGui gui) { gui.ShowDropDown(gui.lastRect, SettingsDropdown, new Padding(0f, 0f, 0f, 0.5f)); } - if (gui.BuildButton(Icon.Plus).WithTooltip(gui, "Create production sheet (Ctrl+" + - ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_T) + ")")) { + if (gui.BuildButton(Icon.Plus).WithTooltip(gui, LSs.CreateProductionSheet.L(ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_T)))) { ProductionTableView.CreateProductionSheet(); } gui.allocator = RectAllocator.RightRow; - if (gui.BuildButton(Icon.DropDown, SchemeColor.None, SchemeColor.Grey).WithTooltip(gui, "List and search all pages (Ctrl+Shift+" + - ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_F) + ")") || showSearchAll) { + if (gui.BuildButton(Icon.DropDown, SchemeColor.None, SchemeColor.Grey).WithTooltip(gui, LSs.ListAndSearchAll.L(ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_F))) || showSearchAll) { showSearchAll = false; updatePageList(); ShowDropDown(gui, gui.lastRect, missingPagesDropdown, new Padding(0f, 0f, 0f, 0.5f), 30f); @@ -331,7 +329,7 @@ public static void BuildSubHeader(ImGui gui, string text) { } } - private static void ShowNeie() => SelectSingleObjectPanel.Select(Database.goods.explorable, "Open NEIE", NeverEnoughItemsPanel.Show); + private static void ShowNeie() => SelectSingleObjectPanel.Select(Database.goods.explorable, LSs.OpenNeie, NeverEnoughItemsPanel.Show); private void SetSearch(SearchQuery searchQuery) { pageSearch = searchQuery; @@ -348,7 +346,7 @@ private void ShowSearch() { } private void BuildSearch(ImGui gui) { - gui.BuildText("Find on page:"); + gui.BuildText(LSs.SearchHeader); gui.AllocateSpacing(); gui.allocator = RectAllocator.RightRow; if (gui.BuildButton(Icon.Close)) { @@ -368,73 +366,73 @@ private void BuildSearch(ImGui gui) { private void SettingsDropdown(ImGui gui) { gui.boxColor = SchemeColor.Background; - if (gui.BuildContextMenuButton("Undo", "Ctrl+" + ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_Z)) && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.Undo, LSs.ShortcutCtrlX.L(ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_Z))) && gui.CloseDropdown()) { project.undo.PerformUndo(); } - if (gui.BuildContextMenuButton("Save", "Ctrl+" + ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_S)) && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.Save, LSs.ShortcutCtrlX.L(ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_S))) && gui.CloseDropdown()) { SaveProject().CaptureException(); } - if (gui.BuildContextMenuButton("Save As") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.SaveAs) && gui.CloseDropdown()) { SaveProjectAs().CaptureException(); } - if (gui.BuildContextMenuButton("Find on page", "Ctrl+" + ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_F)) && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.FindOnPage, LSs.ShortcutCtrlX.L(ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_F))) && gui.CloseDropdown()) { ShowSearch(); } - if (gui.BuildContextMenuButton("Load another project (Same mods)") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.LoadWithSameMods) && gui.CloseDropdown()) { LoadProjectLight(); } - if (gui.BuildContextMenuButton("Return to starting screen") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.ReturnToWelcomeScreen) && gui.CloseDropdown()) { LoadProjectHeavy(); } - BuildSubHeader(gui, "Tools"); - if (gui.BuildContextMenuButton("Milestones") && gui.CloseDropdown()) { + BuildSubHeader(gui, LSs.MenuHeaderTools); + if (gui.BuildContextMenuButton(LSs.Milestones) && gui.CloseDropdown()) { _ = ShowPseudoScreen(new MilestonesPanel()); } - if (gui.BuildContextMenuButton("Preferences") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.MenuPreferences) && gui.CloseDropdown()) { PreferencesScreen.ShowPreviousState(); } - if (gui.BuildContextMenuButton("Summary") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.MenuSummary) && gui.CloseDropdown()) { ShowSummaryTab(); } - if (gui.BuildContextMenuButton("Summary (Legacy)") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.MenuLegacySummary) && gui.CloseDropdown()) { ProjectPageSettingsPanel.Show(null, (name, icon) => Instance.AddProjectPage(name, icon, typeof(ProductionSummary), true, true)); } - if (gui.BuildContextMenuButton("Never Enough Items Explorer", "Ctrl+" + ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_N)) && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.Neie, LSs.ShortcutCtrlX.L(ImGuiUtils.ScanToString(SDL.SDL_Scancode.SDL_SCANCODE_N))) && gui.CloseDropdown()) { ShowNeie(); } - if (gui.BuildContextMenuButton("Dependency Explorer") && gui.CloseDropdown()) { - SelectSingleObjectPanel.Select(Database.objects.explorable, "Open Dependency Explorer", DependencyExplorer.Show); + if (gui.BuildContextMenuButton(LSs.DependencyExplorer) && gui.CloseDropdown()) { + SelectSingleObjectPanel.Select(Database.objects.explorable, LSs.OpenDependencyExplorer, DependencyExplorer.Show); } - if (gui.BuildContextMenuButton("Import page from clipboard", disabled: !ImGuiUtils.HasClipboardText()) && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.ImportFromClipboard, disabled: !ImGuiUtils.HasClipboardText()) && gui.CloseDropdown()) { ProjectPageSettingsPanel.LoadProjectPageFromClipboard(); } - BuildSubHeader(gui, "Extra"); + BuildSubHeader(gui, LSs.MenuHeaderExtra); - if (gui.BuildContextMenuButton("Run Factorio")) { + if (gui.BuildContextMenuButton(LSs.MenuRunFactorio)) { string factorioPath = DataUtils.dataPath + "/../bin/x64/factorio"; string? args = string.IsNullOrEmpty(DataUtils.modsPath) ? null : "--mod-directory \"" + DataUtils.modsPath + "\""; _ = Process.Start(new ProcessStartInfo(factorioPath, args!) { UseShellExecute = true }); // null-forgiving: ProcessStartInfo permits null args. _ = gui.CloseDropdown(); } - if (gui.BuildContextMenuButton("Check for updates") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.MenuCheckForUpdates) && gui.CloseDropdown()) { DoCheckForUpdates(); } - if (gui.BuildContextMenuButton("About YAFC") && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.MenuAbout) && gui.CloseDropdown()) { _ = new AboutScreen(this); } } @@ -477,13 +475,16 @@ public void ForceClose() { } private async Task ConfirmUnsavedChanges() { - string unsavedCount = "You have " + project.unsavedChangesCount + " unsaved changes"; - if (!string.IsNullOrEmpty(project.attachedFileName)) { - unsavedCount += " to " + project.attachedFileName; + string unsavedCount; + if (string.IsNullOrEmpty(project.attachedFileName)) { + unsavedCount = LSs.AlertUnsavedChanges.L(project.unsavedChangesCount); + } + else { + unsavedCount = LSs.AlertUnsavedChangesInFile.L(project.unsavedChangesCount, project.attachedFileName); } saveConfirmationActive = true; - var (hasChoice, choice) = await MessageBox.Show("Save unsaved changes?", unsavedCount, "Save", "Don't save"); + var (hasChoice, choice) = await MessageBox.Show(LSs.QuerySaveChanges, unsavedCount, LSs.Save, LSs.DontSave); saveConfirmationActive = false; if (!hasChoice) { return false; @@ -511,21 +512,21 @@ private static async void DoCheckForUpdates() { var release = JsonSerializer.Deserialize(result)!; string version = release.tag_name.StartsWith('v') ? release.tag_name[1..] : release.tag_name; if (new Version(version) > YafcLib.version) { - var (_, answer) = await MessageBox.Show("New version available!", "There is a new version available: " + release.tag_name, "Visit release page", "Close"); + var (_, answer) = await MessageBox.Show(LSs.NewVersionAvailable, LSs.NewVersionNumber.L(release.tag_name), LSs.VisitReleasePage, LSs.Close); if (answer) { Ui.VisitLink(release.html_url); } return; } - MessageBox.Show("No newer version", "You are running the latest version!", "Ok"); + MessageBox.Show(LSs.NoNewerVersion, LSs.RunningLatestVersion, LSs.Ok); } catch (Exception) { MessageBox.Show((hasAnswer, answer) => { if (answer) { Ui.VisitLink(AboutScreen.Github + "/releases"); } - }, "Network error", "There were an error while checking versions.", "Open releases url", "Close"); + }, LSs.NetworkError, LSs.ErrorWhileCheckingForNewVersion, LSs.VisitReleasePage, LSs.Close); } } @@ -563,7 +564,7 @@ public void ShowSummaryTab() { if (summaryPage == null) { summaryPage = new ProjectPage(project, typeof(Summary), SummaryGuid) { - name = "Summary", + name = LSs.MenuSummary, }; project.pages.Add(summaryPage); } @@ -637,9 +638,9 @@ public bool KeyDown(SDL.SDL_Keysym key) { } private async Task SaveProjectAs() { - string? projectPath = await new FilesystemScreen("Save project", "Save project as", "Save", + string? projectPath = await new FilesystemScreen(LSs.SaveProjectWindowTitle, LSs.SaveProjectWindowHeader, LSs.Save, string.IsNullOrEmpty(project.attachedFileName) ? null : Path.GetDirectoryName(project.attachedFileName), - FilesystemScreen.Mode.SelectOrCreateFile, "project", this, null, "yafc"); + FilesystemScreen.Mode.SelectOrCreateFile, LSs.DefaultFileName, this, null, "yafc"); if (projectPath != null) { project.Save(projectPath); Preferences.Instance.AddProject(DataUtils.dataPath, DataUtils.modsPath, projectPath, DataUtils.netProduction); @@ -664,8 +665,8 @@ private async void LoadProjectLight() { } string? projectDirectory = string.IsNullOrEmpty(project.attachedFileName) ? null : Path.GetDirectoryName(project.attachedFileName); - string? path = await new FilesystemScreen("Load project", "Load another .yafc project", "Select", projectDirectory, - FilesystemScreen.Mode.SelectOrCreateFile, "project", this, null, "yafc"); + string? path = await new FilesystemScreen(LSs.LoadProjectWindowTitle, LSs.LoadProjectWindowHeader, LSs.SelectProject, projectDirectory, + FilesystemScreen.Mode.SelectOrCreateFile, LSs.DefaultFileName, this, null, "yafc"); if (path == null) { return; @@ -678,7 +679,7 @@ private async void LoadProjectLight() { SetProject(project); } catch (Exception ex) { - errors.Exception(ex, "Critical loading exception", ErrorSeverity.Important); + errors.Exception(ex, LSs.ErrorCriticalLoadingException, ErrorSeverity.Important); } if (errors.severity != ErrorSeverity.None) { ErrorListPanel.Show(errors); @@ -749,7 +750,7 @@ public void ShowTooltip(ImGui gui, ProjectPage? page, bool isMiddleEdit, Rect re ShowTooltip(gui, rect, x => { pageView.BuildPageTooltip(x, page.content); if (isMiddleEdit) { - x.BuildText("Middle mouse button to edit", TextBlockDisplayStyle.WrappedText with { Color = SchemeColor.BackgroundTextFaint }); + x.BuildText(LSs.SearchAllMiddleMouseToEditHint, TextBlockDisplayStyle.WrappedText with { Color = SchemeColor.BackgroundTextFaint }); } }); } diff --git a/Yafc/Windows/MilestonesEditor.cs b/Yafc/Windows/MilestonesEditor.cs index 4485cca6..9b8942c5 100644 --- a/Yafc/Windows/MilestonesEditor.cs +++ b/Yafc/Windows/MilestonesEditor.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Numerics; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -44,15 +45,14 @@ private void MilestoneDrawer(ImGui gui, FactorioObject element, int index) { } public override void Build(ImGui gui) { - BuildHeader(gui, "Milestone editor"); + BuildHeader(gui, LSs.MilestoneEditor); milestoneList.Build(gui); - string milestoneHintText = "Hint: You can reorder milestones. When an object is locked behind a milestone, the first inaccessible milestone will be shown. " + - "Also when there is a choice between different milestones, first will be chosen"; + string milestoneHintText = LSs.MilestoneDescription; gui.BuildText(milestoneHintText, TextBlockDisplayStyle.WrappedText with { Color = SchemeColor.BackgroundTextFaint }); using (gui.EnterRow()) { - if (gui.BuildButton("Auto sort milestones", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.MilestoneAutoSort, SchemeColor.Grey)) { ErrorCollector collector = new ErrorCollector(); Milestones.Instance.ComputeWithParameters(Project.current, collector, [.. Project.current.settings.milestones], true); @@ -62,8 +62,8 @@ public override void Build(ImGui gui) { milestoneList.RebuildContents(); } - if (gui.BuildButton("Add milestone")) { - SelectMultiObjectPanel.Select(Database.objects.explorable.Except(Project.current.settings.milestones), "Add new milestone", AddMilestone); + if (gui.BuildButton(LSs.MilestoneAdd)) { + SelectMultiObjectPanel.Select(Database.objects.explorable.Except(Project.current.settings.milestones), LSs.MilestoneAddNew, AddMilestone); } } } @@ -72,7 +72,7 @@ private void AddMilestone(FactorioObject obj) { var settings = Project.current.settings; if (settings.milestones.Contains(obj)) { - MessageBox.Show("Cannot add milestone", "Milestone already exists", "Ok"); + MessageBox.Show(LSs.MilestoneCannotAdd, LSs.MilestoneCannotAddAlreadyExists, LSs.Ok); return; } diff --git a/Yafc/Windows/MilestonesPanel.cs b/Yafc/Windows/MilestonesPanel.cs index d5fd95e9..faf68bdd 100644 --- a/Yafc/Windows/MilestonesPanel.cs +++ b/Yafc/Windows/MilestonesPanel.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -39,26 +40,23 @@ public class MilestonesPanel : PseudoScreen { public override void Build(ImGui gui) { instance = milestonesWidget; gui.spacing = 1f; - BuildHeader(gui, "Milestones"); - gui.BuildText("Please select objects that you already have access to:"); + BuildHeader(gui, LSs.Milestones); + gui.BuildText(LSs.MilestonesHeader); gui.AllocateSpacing(2f); milestonesWidget.Build(gui); gui.AllocateSpacing(2f); - gui.BuildText("For your convenience, YAFC will show objects you DON'T have access to based on this selection", TextBlockDisplayStyle.WrappedText); - gui.BuildText("These are called 'Milestones'. By default all science packs and locations are added as milestones, but this does not have to be this way! " + - "You can define your own milestones: Any item, recipe, entity or technology may be added as a milestone. For example you can add advanced " + - "electronic circuits as a milestone, and YAFC will display everything that is locked behind those circuits", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.MilestonesDescription, TextBlockDisplayStyle.WrappedText); using (gui.EnterRow()) { - if (gui.BuildButton("Edit milestones", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.MilestonesEdit, SchemeColor.Grey)) { MilestonesEditor.Show(); } - if (gui.BuildButton("Edit tech progression settings")) { + if (gui.BuildButton(LSs.MilestonesEditSettings)) { Close(); PreferencesScreen.ShowProgression(); } - if (gui.RemainingRow().BuildButton("Done")) { + if (gui.RemainingRow().BuildButton(LSs.Done)) { Close(); } } diff --git a/Yafc/Windows/NeverEnoughItemsPanel.cs b/Yafc/Windows/NeverEnoughItemsPanel.cs index 2889b48f..2be42d99 100644 --- a/Yafc/Windows/NeverEnoughItemsPanel.cs +++ b/Yafc/Windows/NeverEnoughItemsPanel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -121,7 +122,7 @@ private void DrawIngredients(ImGui gui, Recipe recipe) { foreach (var ingredient in recipe.ingredients) { if (gui.BuildFactorioObjectWithAmount(ingredient.goods, ingredient.amount, ButtonDisplayStyle.NeieSmall) == Click.Left) { if (ingredient.variants != null) { - gui.ShowDropDown(imGui => imGui.BuildInlineObjectListAndButton(ingredient.variants, SetItem, new("Accepted fluid variants"))); + gui.ShowDropDown(imGui => imGui.BuildInlineObjectListAndButton(ingredient.variants, SetItem, new(LSs.NeieAcceptedVariants))); } else { changing = ingredient.goods; @@ -186,10 +187,10 @@ private void DrawRecipeEntry(ImGui gui, RecipeEntry entry, bool production) { } float bh = CostAnalysis.GetBuildingHours(recipe, entry.recipeFlow); if (bh > 20) { - gui.BuildText(DataUtils.FormatAmount(bh, UnitOfMeasure.None) + "bh", TextBlockDisplayStyle.Centered); + gui.BuildText(LSs.NeieBuildingHoursSuffix.L(DataUtils.FormatAmount(bh, UnitOfMeasure.None)), TextBlockDisplayStyle.Centered); _ = gui.BuildButton(gui.lastRect, SchemeColor.None, SchemeColor.Grey) - .WithTooltip(gui, "Building-hours.\nAmount of building-hours required for all researches assuming crafting speed of 1"); + .WithTooltip(gui, LSs.NeieBuildingHoursDescription); } } gui.AllocateSpacing(); @@ -255,7 +256,7 @@ private void DrawRecipeEntry(ImGui gui, RecipeEntry entry, bool production) { private void DrawEntryFooter(ImGui gui, bool production) { if (!production && current.fuelFor.Length > 0) { using (gui.EnterGroup(new Padding(0.5f), RectAllocator.LeftAlign)) { - gui.BuildText(current.fuelValue > 0f ? "Fuel value " + DataUtils.FormatAmount(current.fuelValue, UnitOfMeasure.Megajoule) + " can be used for:" : "Can be used to fuel:"); + gui.BuildText(current.fuelValue > 0f ? LSs.FuelValueCanBeUsed.L(DataUtils.FormatAmount(current.fuelValue, UnitOfMeasure.Megajoule)) : LSs.FuelValueZeroCanBeUsed); using var grid = gui.EnterInlineGrid(3f); foreach (var fuelUsage in current.fuelFor) { grid.Next(); @@ -285,10 +286,10 @@ private void DrawEntryList(ImGui gui, ReadOnlySpan entries, bool pr if (status < showRecipesRange) { DrawEntryFooter(gui, production); footerDrawn = true; - gui.BuildText(entry.entryStatus == EntryStatus.Special ? "Show special recipes (barreling / voiding)" : - entry.entryStatus == EntryStatus.NotAccessibleWithCurrentMilestones ? "There are more recipes, but they are locked based on current milestones" : - "There are more recipes but they are inaccessible", TextBlockDisplayStyle.WrappedText); - if (gui.BuildButton("Show more recipes")) { + gui.BuildText(entry.entryStatus == EntryStatus.Special ? LSs.NeieShowSpecialRecipes : + entry.entryStatus == EntryStatus.NotAccessibleWithCurrentMilestones ? LSs.NeieShowLockedRecipes : + LSs.NeieShowInaccessibleRecipes, TextBlockDisplayStyle.WrappedText); + if (gui.BuildButton(LSs.NeieShowMoreRecipes)) { ChangeShowStatus(status); } @@ -298,8 +299,10 @@ private void DrawEntryList(ImGui gui, ReadOnlySpan entries, bool pr if (status < prevEntryStatus) { prevEntryStatus = status; using (gui.EnterRow()) { - gui.BuildText(status == EntryStatus.Special ? "Special recipes:" : status == EntryStatus.NotAccessibleWithCurrentMilestones ? "Locked recipes:" : "Inaccessible recipes:"); - if (gui.BuildLink("hide")) { + gui.BuildText(status == EntryStatus.Special ? LSs.NeieSpecialRecipes : + status == EntryStatus.NotAccessibleWithCurrentMilestones ? LSs.NeieLockedRecipes : LSs.NeieInaccessibleRecipes); + + if (gui.BuildLink(LSs.NeieHideRecipes)) { ChangeShowStatus(status + 1); } } @@ -330,7 +333,7 @@ private void DrawEntryList(ImGui gui, ReadOnlySpan entries, bool pr private void BuildItemUsages(ImGui gui) => DrawEntryList(gui, usages, false); public override void Build(ImGui gui) { - BuildHeader(gui, "Never Enough Items Explorer"); + BuildHeader(gui, LSs.NeieHeader); using (gui.EnterRow()) { if (recent.Count == 0) { _ = gui.AllocateRect(0f, 3f); @@ -356,27 +359,24 @@ public override void Build(ImGui gui) { } if (gui.BuildFactorioObjectButtonBackground(gui.lastRect, current, SchemeColor.Grey) == Click.Left) { - SelectSingleObjectPanel.Select(Database.goods.explorable, "Select item", SetItem); + SelectSingleObjectPanel.Select(Database.goods.explorable, LSs.SelectItem, SetItem); } using (var split = gui.EnterHorizontalSplit(2)) { split.Next(); - gui.BuildText("Production:", Font.subheader); + gui.BuildText(LSs.NeieProduction, Font.subheader); productionList.Build(gui); split.Next(); - gui.BuildText("Usages:", Font.subheader); + gui.BuildText(LSs.NeieUsage, Font.subheader); usageList.Build(gui); } CheckChanging(); using (gui.EnterRow()) { - if (gui.BuildLink("What do colored bars mean?")) { - MessageBox.Show("How to read colored bars", - "Blue bar means estimated production or consumption of the thing you selected. Blue bar at 50% means that that recipe produces(consumes) 50% of the product.\n\n" + - "Orange bar means estimated recipe efficiency. If it is not full, the recipe looks inefficient to YAFC.\n\n" + - "It is possible for a recipe to be efficient but not useful - for example a recipe that produces something that is not useful.\n\n" + - "YAFC only estimates things that are required for science recipes. So buildings, belts, weapons, fuel - are not shown in estimations.", "Ok"); + if (gui.BuildLink(LSs.NeieColoredBarsLink)) { + MessageBox.Show(LSs.NeieHowToReadColoredBars, + LSs.NeieColoredBarsDescription, LSs.Ok); } - if (gui.BuildCheckBox("Current milestones info", atCurrentMilestones, out atCurrentMilestones, allocator: RectAllocator.RightRow)) { + if (gui.BuildCheckBox(LSs.NeieCurrentMilestonesCheckbox, atCurrentMilestones, out atCurrentMilestones, allocator: RectAllocator.RightRow)) { Refresh(); } } diff --git a/Yafc/Windows/PreferencesScreen.cs b/Yafc/Windows/PreferencesScreen.cs index b2d2cf40..dd5bb7f9 100644 --- a/Yafc/Windows/PreferencesScreen.cs +++ b/Yafc/Windows/PreferencesScreen.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -11,14 +12,14 @@ public class PreferencesScreen : PseudoScreen { private const int GENERAL_PAGE = 0, PROGRESSION_PAGE = 1; private readonly TabControl tabControl; - private PreferencesScreen() => tabControl = new(("General", DrawGeneral), ("Progression", DrawProgression)); + private PreferencesScreen() => tabControl = new((LSs.PreferencesTabGeneral, DrawGeneral), (LSs.PreferencesTabProgression, DrawProgression)); public override void Build(ImGui gui) { - BuildHeader(gui, "Preferences"); + BuildHeader(gui, LSs.Preferences); gui.AllocateSpacing(); tabControl.Build(gui); gui.AllocateSpacing(); - if (gui.BuildButton("Done")) { + if (gui.BuildButton(LSs.Done)) { Close(); } @@ -36,43 +37,43 @@ public override void Build(ImGui gui) { private void DrawProgression(ImGui gui) { ProjectPreferences preferences = Project.current.preferences; - ChooseObject(gui, "Default belt:", Database.allBelts, preferences.defaultBelt, s => { + ChooseObject(gui, LSs.PreferencesDefaultBelt, Database.allBelts, preferences.defaultBelt, s => { preferences.RecordUndo().defaultBelt = s; gui.Rebuild(); }); - ChooseObject(gui, "Default inserter:", Database.allInserters, preferences.defaultInserter, s => { + ChooseObject(gui, LSs.PreferencesDefaultInserter, Database.allInserters, preferences.defaultInserter, s => { preferences.RecordUndo().defaultInserter = s; gui.Rebuild(); }); using (gui.EnterRow()) { - gui.BuildText("Inserter capacity:", topOffset: 0.5f); + gui.BuildText(LSs.PreferencesInserterCapacity, topOffset: 0.5f); if (gui.BuildIntegerInput(preferences.inserterCapacity, out int newCapacity)) { preferences.RecordUndo().inserterCapacity = newCapacity; } } - ChooseObjectWithNone(gui, "Target technology for cost analysis: ", Database.technologies.all, preferences.targetTechnology, x => { + ChooseObjectWithNone(gui, LSs.PreferencesTargetTechnology, Database.technologies.all, preferences.targetTechnology, x => { preferences.RecordUndo().targetTechnology = x; gui.Rebuild(); }, width: 25f); gui.AllocateSpacing(); using (gui.EnterRow()) { - gui.BuildText("Mining productivity bonus: ", topOffset: 0.5f); + gui.BuildText(LSs.PreferencesMiningProductivityBonus, topOffset: 0.5f); DisplayAmount amount = new(Project.current.settings.miningProductivity, UnitOfMeasure.Percent); if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput) && amount.Value >= 0) { Project.current.settings.RecordUndo().miningProductivity = amount.Value; } } using (gui.EnterRow()) { - gui.BuildText("Research speed bonus: ", topOffset: 0.5f); + gui.BuildText(LSs.PreferencesResearchSpeedBonus, topOffset: 0.5f); DisplayAmount amount = new(Project.current.settings.researchSpeedBonus, UnitOfMeasure.Percent); if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput) && amount.Value >= 0) { Project.current.settings.RecordUndo().researchSpeedBonus = amount.Value; } } using (gui.EnterRow()) { - gui.BuildText("Research productivity bonus: ", topOffset: 0.5f); + gui.BuildText(LSs.PreferencesResearchProductivityBonus, topOffset: 0.5f); DisplayAmount amount = new(Project.current.settings.researchProductivity, UnitOfMeasure.Percent); if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput) && amount.Value >= 0) { Project.current.settings.RecordUndo().researchProductivity = amount.Value; @@ -98,7 +99,7 @@ private void DrawTechnology(ImGui gui, Technology tech, int _) { using (gui.EnterRow()) { gui.allocator = RectAllocator.LeftRow; gui.BuildFactorioObjectButton(tech, ButtonDisplayStyle.Default); - gui.BuildText($"{tech.locName} Level: "); + gui.BuildText(LSs.PreferencesTechnologyLevel.L(tech.locName)); int currentLevel = Project.current.settings.productivityTechnologyLevels.GetValueOrDefault(tech, 0); if (gui.BuildIntegerInput(currentLevel, out int newLevel) && newLevel >= 0) { Project.current.settings.RecordUndo().productivityTechnologyLevels[tech] = newLevel; @@ -111,21 +112,21 @@ private static void DrawGeneral(ImGui gui) { ProjectSettings settings = Project.current.settings; bool newValue; - gui.BuildText("Unit of time:", Font.subheader); + gui.BuildText(LSs.PrefsUnitOfTime, Font.subheader); using (gui.EnterRow()) { - if (gui.BuildRadioButton("Second", preferences.time == 1)) { + if (gui.BuildRadioButton(LSs.PrefsUnitSeconds, preferences.time == 1)) { preferences.RecordUndo(true).time = 1; } - if (gui.BuildRadioButton("Minute", preferences.time == 60)) { + if (gui.BuildRadioButton(LSs.PrefsUnitMinutes, preferences.time == 60)) { preferences.RecordUndo(true).time = 60; } - if (gui.BuildRadioButton("Hour", preferences.time == 3600)) { + if (gui.BuildRadioButton(LSs.PrefsTimeUnitHours, preferences.time == 3600)) { preferences.RecordUndo(true).time = 3600; } - if (gui.BuildRadioButton("Custom", preferences.time is not 1 and not 60 and not 3600)) { + if (gui.BuildRadioButton(LSs.PrefsTimeUnitCustom, preferences.time is not 1 and not 60 and not 3600)) { preferences.RecordUndo(true).time = 0; } @@ -134,13 +135,13 @@ private static void DrawGeneral(ImGui gui) { } } gui.AllocateSpacing(1f); - gui.BuildText("Item production/consumption:", Font.subheader); + gui.BuildText(LSs.PrefsHeaderItemUnits, Font.subheader); BuildUnitPerTime(gui, false, preferences); - gui.BuildText("Fluid production/consumption:", Font.subheader); + gui.BuildText(LSs.PrefsHeaderFluidUnits, Font.subheader); BuildUnitPerTime(gui, true, preferences); - using (gui.EnterRowWithHelpIcon("0% for off, 100% for old default")) { - gui.BuildText("Pollution cost modifier", topOffset: 0.5f); + using (gui.EnterRowWithHelpIcon(LSs.PrefsPollutionCostHint)) { + gui.BuildText(LSs.PrefsPollutionCost, topOffset: 0.5f); DisplayAmount amount = new(settings.PollutionCostModifier, UnitOfMeasure.Percent); if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput) && amount.Value >= 0) { settings.RecordUndo().PollutionCostModifier = amount.Value; @@ -148,10 +149,10 @@ private static void DrawGeneral(ImGui gui) { } } - string iconScaleMessage = "Some mod icons have little or no transparency, hiding the background color. This setting reduces the size of icons that could hide link information."; + string iconScaleMessage = LSs.PrefsIconScaleHint; using (gui.EnterRowWithHelpIcon(iconScaleMessage)) { - gui.BuildText("Display scale for linkable icons", topOffset: 0.5f); + gui.BuildText(LSs.PrefsIconScale, topOffset: 0.5f); DisplayAmount amount = new(preferences.iconScale, UnitOfMeasure.Percent); if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput) && amount.Value > 0 && amount.Value <= 1) { preferences.RecordUndo().iconScale = amount.Value; @@ -162,10 +163,9 @@ private static void DrawGeneral(ImGui gui) { // Don't show this preference if it isn't relevant. // (Takes ~3ms for pY, which would concern me in the regular UI, but should be fine here.) if (Database.objects.all.Any(o => Milestones.Instance.GetMilestoneResult(o).PopCount() > 22)) { - string overlapMessage = "Some tooltips may want to show multiple rows of milestones. Increasing this number will draw fewer lines in some tooltips, by forcing the milestones to overlap.\n\n" - + "Minimum: 22\nDefault: 28"; + string overlapMessage = LSs.PrefsMilestonesPerLineHint; using (gui.EnterRowWithHelpIcon(overlapMessage)) { - gui.BuildText("Maximum milestones per line in tooltips:", topOffset: 0.5f); + gui.BuildText(LSs.PrefsMilestonesPerLine, topOffset: 0.5f); if (gui.BuildIntegerInput(preferences.maxMilestonesPerTooltipLine, out int newIntValue) && newIntValue >= 22) { preferences.RecordUndo().maxMilestonesPerTooltipLine = newIntValue; gui.Rebuild(); @@ -174,9 +174,9 @@ private static void DrawGeneral(ImGui gui) { } using (gui.EnterRow()) { - gui.BuildText("Reactor layout:", topOffset: 0.5f); - if (gui.BuildTextInput(settings.reactorSizeX + "x" + settings.reactorSizeY, out string newSize, null, delayed: true)) { - int px = newSize.IndexOf('x'); + gui.BuildText(LSs.PrefsReactorLayout, topOffset: 0.5f); + if (gui.BuildTextInput(settings.reactorSizeX + LSs.PrefsReactorXYSeparator + settings.reactorSizeY, out string newSize, null, delayed: true)) { + int px = newSize.IndexOf(LSs.PrefsReactorXYSeparator); if (px < 0 && int.TryParse(newSize, out int value)) { settings.RecordUndo().reactorSizeX = value; settings.reactorSizeY = value; @@ -188,8 +188,8 @@ private static void DrawGeneral(ImGui gui) { } } - using (gui.EnterRowWithHelpIcon("Set this to match the spoiling rate you selected when starting your game. 10% is slow spoiling, and 1000% (1k%) is fast spoiling.")) { - gui.BuildText("Spoiling rate:", topOffset: 0.5f); + using (gui.EnterRowWithHelpIcon(LSs.PrefsSpoilingRateHint)) { + gui.BuildText(LSs.PrefsSpoilingRate, topOffset: 0.5f); DisplayAmount amount = new(settings.spoilingRate, UnitOfMeasure.Percent); if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput)) { settings.RecordUndo().spoilingRate = Math.Clamp(amount.Value, .1f, 10); @@ -199,17 +199,17 @@ private static void DrawGeneral(ImGui gui) { gui.AllocateSpacing(); - if (gui.BuildCheckBox("Show milestone overlays on inaccessible objects", preferences.showMilestoneOnInaccessible, out newValue)) { + if (gui.BuildCheckBox(LSs.PrefsShowInaccessibleMilestoneOverlays, preferences.showMilestoneOnInaccessible, out newValue)) { preferences.RecordUndo().showMilestoneOnInaccessible = newValue; } - if (gui.BuildCheckBox("Dark mode", Preferences.Instance.darkMode, out newValue)) { + if (gui.BuildCheckBox(LSs.PrefsDarkMode, Preferences.Instance.darkMode, out newValue)) { Preferences.Instance.darkMode = newValue; Preferences.Instance.Save(); RenderingUtils.SetColorScheme(newValue); } - if (gui.BuildCheckBox("Enable autosave (Saves when the window loses focus)", Preferences.Instance.autosaveEnabled, out newValue)) { + if (gui.BuildCheckBox(LSs.PrefsAutosave, Preferences.Instance.autosaveEnabled, out newValue)) { Preferences.Instance.autosaveEnabled = newValue; } } @@ -243,26 +243,26 @@ private static void ChooseObjectWithNone(ImGui gui, string text, T[] list, T? private static void BuildUnitPerTime(ImGui gui, bool fluid, ProjectPreferences preferences) { DisplayAmount unit = fluid ? preferences.fluidUnit : preferences.itemUnit; - if (gui.BuildRadioButton("Simple Amount" + preferences.GetPerTimeUnit().suffix, unit == 0f)) { + if (gui.BuildRadioButton(LSs.PrefsGoodsUnitSimple.L(preferences.GetPerTimeUnit().suffix), unit == 0f)) { unit = 0f; } using (gui.EnterRow()) { - if (gui.BuildRadioButton("Custom: 1 unit equals", unit != 0f)) { + if (gui.BuildRadioButton(LSs.PrefsGoodsUnitCustom, unit != 0f)) { unit = 1f; } gui.AllocateSpacing(); gui.allocator = RectAllocator.RightRow; if (!fluid) { - if (gui.BuildButton("Set from belt")) { + if (gui.BuildButton(LSs.PrefsGoodsUnitFromBelt)) { gui.BuildObjectSelectDropDown(Database.allBelts, setBelt => { _ = preferences.RecordUndo(true); preferences.itemUnit = setBelt.beltItemsPerSecond; - }, new("Select belt", DataUtils.DefaultOrdering, ExtraText: b => DataUtils.FormatAmount(b.beltItemsPerSecond, UnitOfMeasure.PerSecond))); + }, new(LSs.PrefsSelectBelt, DataUtils.DefaultOrdering, ExtraText: b => DataUtils.FormatAmount(b.beltItemsPerSecond, UnitOfMeasure.PerSecond))); } } - gui.BuildText("per second"); + gui.BuildText(LSs.PerSecondSuffixLong); _ = gui.BuildFloatInput(unit, TextBoxDisplayStyle.DefaultTextInput); } gui.AllocateSpacing(1f); diff --git a/Yafc/Windows/ProjectPageSettingsPanel.cs b/Yafc/Windows/ProjectPageSettingsPanel.cs index d4e30c76..67f3bfe1 100644 --- a/Yafc/Windows/ProjectPageSettingsPanel.cs +++ b/Yafc/Windows/ProjectPageSettingsPanel.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.Json; using SDL2; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -27,13 +28,13 @@ private ProjectPageSettingsPanel(ProjectPage? editingPage, Action setIcon) { - _ = gui.BuildTextInput(name, out name, "Input name", setKeyboardFocus: editingPage == null ? SetKeyboardFocus.OnFirstPanelDraw : SetKeyboardFocus.No); + _ = gui.BuildTextInput(name, out name, LSs.PageSettingsNameHint, setKeyboardFocus: editingPage == null ? SetKeyboardFocus.OnFirstPanelDraw : SetKeyboardFocus.No); if (gui.BuildFactorioObjectButton(icon, new ButtonDisplayStyle(4f, MilestoneDisplay.None, SchemeColor.Grey) with { UseScaleSetting = false }) == Click.Left) { - SelectSingleObjectPanel.Select(Database.objects.all, "Select icon", setIcon); + SelectSingleObjectPanel.Select(Database.objects.all, LSs.SelectIcon, setIcon); } if (icon == null && gui.isBuilding) { - gui.DrawText(gui.lastRect, "And select icon", RectAlignment.Middle); + gui.DrawText(gui.lastRect, LSs.PageSettingsIconHint, RectAlignment.Middle); } } @@ -41,31 +42,31 @@ private void Build(ImGui gui, Action setIcon) { public override void Build(ImGui gui) { gui.spacing = 3f; - BuildHeader(gui, editingPage == null ? "Create new page" : "Edit page icon and name"); + BuildHeader(gui, editingPage == null ? LSs.PageSettingsCreateHeader : LSs.PageSettingsEditHeader); Build(gui, s => { icon = s; Rebuild(); }); using (gui.EnterRow(0.5f, RectAllocator.RightRow)) { - if (editingPage == null && gui.BuildButton("Create", active: !string.IsNullOrEmpty(name))) { + if (editingPage == null && gui.BuildButton(LSs.Create, active: !string.IsNullOrEmpty(name))) { ReturnPressed(); } - if (editingPage != null && gui.BuildButton("OK", active: !string.IsNullOrEmpty(name))) { + if (editingPage != null && gui.BuildButton(LSs.Ok, active: !string.IsNullOrEmpty(name))) { ReturnPressed(); } - if (gui.BuildButton("Cancel", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.Cancel, SchemeColor.Grey)) { Close(); } - if (editingPage != null && gui.BuildButton("Other tools", SchemeColor.Grey, active: !string.IsNullOrEmpty(name))) { + if (editingPage != null && gui.BuildButton(LSs.PageSettingsOther, SchemeColor.Grey, active: !string.IsNullOrEmpty(name))) { gui.ShowDropDown(OtherToolsDropdown); } gui.allocator = RectAllocator.LeftRow; - if (editingPage != null && gui.BuildRedButton("Delete page")) { + if (editingPage != null && gui.BuildRedButton(LSs.DeletePage)) { if (editingPage.canDelete) { Project.current.RemovePage(editingPage); } @@ -94,7 +95,7 @@ protected override void ReturnPressed() { } private void OtherToolsDropdown(ImGui gui) { - if (editingPage!.guid != MainScreen.SummaryGuid && gui.BuildContextMenuButton("Duplicate page")) { // null-forgiving: This dropdown is not shown when editingPage is null. + if (editingPage!.guid != MainScreen.SummaryGuid && gui.BuildContextMenuButton(LSs.DuplicatePage)) { // null-forgiving: This dropdown is not shown when editingPage is null. _ = gui.CloseDropdown(); var project = editingPage.owner; if (ClonePage(editingPage) is { } serializedCopy) { @@ -106,7 +107,7 @@ private void OtherToolsDropdown(ImGui gui) { } } - if (editingPage.guid != MainScreen.SummaryGuid && gui.BuildContextMenuButton("Share (export string to clipboard)")) { + if (editingPage.guid != MainScreen.SummaryGuid && gui.BuildContextMenuButton(LSs.ExportPageToClipboard)) { _ = gui.CloseDropdown(); var data = JsonUtils.SaveToJson(editingPage); using MemoryStream targetStream = new MemoryStream(); @@ -123,14 +124,14 @@ private void OtherToolsDropdown(ImGui gui) { _ = SDL.SDL_SetClipboardText(encoded); } - if (editingPage == MainScreen.Instance.activePage && gui.BuildContextMenuButton("Make full page screenshot")) { + if (editingPage == MainScreen.Instance.activePage && gui.BuildContextMenuButton(LSs.PageSettingsScreenshot)) { // null-forgiving: editingPage is not null, so neither is activePage, and activePage and activePageView become null or not-null together. (see MainScreen.ChangePage) var screenshot = MainScreen.Instance.activePageView!.GenerateFullPageScreenshot(); _ = new ImageSharePanel(screenshot, editingPage.name); _ = gui.CloseDropdown(); } - if (gui.BuildContextMenuButton("Export calculations (to clipboard)")) { + if (gui.BuildContextMenuButton(LSs.PageSettingsExportCalculations)) { ExportPage(editingPage); _ = gui.CloseDropdown(); } @@ -168,9 +169,9 @@ public ExportRecipe(RecipeRow row) { Recipe = row.recipe.QualityName(); Building = ObjectWithQuality.Get(row.entity); BuildingCount = row.buildingCount; - Fuel = new ExportMaterial(row.fuel?.QualityName() ?? "", row.FuelInformation.Amount); - Inputs = row.Ingredients.Select(i => new ExportMaterial(i.Goods?.QualityName() ?? "Recipe disabled", i.Amount)); - Outputs = row.Products.Select(p => new ExportMaterial(p.Goods?.QualityName() ?? "Recipe disabled", p.Amount)); + Fuel = new ExportMaterial(row.fuel?.QualityName() ?? LSs.ExportNoFuelSelected, row.FuelInformation.Amount); + Inputs = row.Ingredients.Select(i => new ExportMaterial(i.Goods?.QualityName() ?? LSs.ExportRecipeDisabled, i.Amount)); + Outputs = row.Products.Select(p => new ExportMaterial(p.Goods?.QualityName() ?? LSs.ExportRecipeDisabled, p.Amount)); Beacon = ObjectWithQuality.Get(row.usedModules.beacon); BeaconCount = row.usedModules.beaconCount; @@ -231,19 +232,19 @@ public static void LoadProjectPageFromClipboard() { Version version = new Version(DataUtils.ReadLine(bytes, ref index) ?? ""); if (version > YafcLib.version) { - collector.Error("String was created with the newer version of YAFC (" + version + "). Data may be lost.", ErrorSeverity.Important); + collector.Error(LSs.AlertImportPageNewerVersion.L(version), ErrorSeverity.Important); } _ = DataUtils.ReadLine(bytes, ref index); // reserved 1 if (DataUtils.ReadLine(bytes, ref index) != "") // reserved 2 but this time it is required to be empty { - throw new NotSupportedException("Share string was created with future version of YAFC (" + version + ") and is incompatible"); + throw new NotSupportedException(LSs.AlertImportPageIncompatibleVersion.L(version)); } page = JsonUtils.LoadFromJson(new ReadOnlySpan(bytes, index, (int)ms.Length - index), project, collector); } catch (Exception ex) { - collector.Exception(ex, "Clipboard text does not contain valid YAFC share string", ErrorSeverity.Critical); + collector.Exception(ex, LSs.AlertImportPageInvalidString, ErrorSeverity.Critical); } if (page != null) { @@ -263,8 +264,8 @@ public static void LoadProjectPageFromClipboard() { project.RecordUndo().pages.Add(page); MainScreen.Instance.SetActivePage(page); - }, "Page already exists", - "Looks like this page already exists with name '" + existing.name + "'. Would you like to replace it or import as copy?", "Replace", "Import as copy"); + }, LSs.ImportPageAlreadyExists, + LSs.ImportPageAlreadyExistsLong.L(existing.name), LSs.Replace, LSs.ImportAsCopy); } else { project.RecordUndo().pages.Add(page); diff --git a/Yafc/Windows/SelectMultiObjectPanel.cs b/Yafc/Windows/SelectMultiObjectPanel.cs index a88611ac..19630a7c 100644 --- a/Yafc/Windows/SelectMultiObjectPanel.cs +++ b/Yafc/Windows/SelectMultiObjectPanel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Numerics; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -76,10 +77,10 @@ protected override void NonNullElementDrawer(ImGui gui, FactorioObject element) public override void Build(ImGui gui) { base.Build(gui); using (gui.EnterGroup(default, RectAllocator.Center)) { - if (gui.BuildButton("OK")) { + if (gui.BuildButton(LSs.Ok)) { CloseWithResult(results); } - gui.BuildText("Hint: ctrl+click to select multiple", TextBlockDisplayStyle.HintText); + gui.BuildText(LSs.SelectMultipleObjectsHint, TextBlockDisplayStyle.HintText); } } diff --git a/Yafc/Windows/SelectObjectPanel.cs b/Yafc/Windows/SelectObjectPanel.cs index bfe5e5c5..f55f0bf2 100644 --- a/Yafc/Windows/SelectObjectPanel.cs +++ b/Yafc/Windows/SelectObjectPanel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Numerics; using SDL2; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -111,7 +112,7 @@ public override void Build(ImGui gui) { gui.AllocateSpacing(); } - if (gui.BuildSearchBox(list.filter, out var newFilter, "Start typing for search", setKeyboardFocus: SetKeyboardFocus.OnFirstPanelDraw)) { + if (gui.BuildSearchBox(list.filter, out var newFilter, LSs.TypeForSearchHint, setKeyboardFocus: SetKeyboardFocus.OnFirstPanelDraw)) { list.filter = newFilter; } diff --git a/Yafc/Windows/ShoppingListScreen.cs b/Yafc/Windows/ShoppingListScreen.cs index 033d5c02..2967842a 100644 --- a/Yafc/Windows/ShoppingListScreen.cs +++ b/Yafc/Windows/ShoppingListScreen.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using Yafc.Blueprints; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -44,7 +45,7 @@ private ShoppingListScreen(List recipes) : base(42) { private void ElementDrawer(ImGui gui, (IObjectWithQuality obj, float count) element, int index) { using (gui.EnterRow()) { gui.BuildFactorioObjectIcon(element.obj, new IconDisplayStyle(2, MilestoneDisplay.Contained, false)); - gui.RemainingRow().BuildText("x" + DataUtils.FormatAmount(element.count, UnitOfMeasure.None) + ": " + element.obj.target.locName); + gui.RemainingRow().BuildText(LSs.ShoppingListCountOfItems.L(DataUtils.FormatAmount(element.count, UnitOfMeasure.None), element.obj.target.locName)); } _ = gui.BuildFactorioObjectButtonBackground(gui.lastRect, element.obj); } @@ -109,29 +110,29 @@ private void RebuildData() { .ToList(); } - private static readonly (string, string?)[] displayStateOptions = [ - ("Total buildings", "Display the total number of buildings required, ignoring the built building count."), - ("Built buildings", "Display the number of buildings that are reported in built building count."), - ("Missing buildings", "Display the number of additional buildings that need to be built.")]; - private static readonly (string, string?)[] assumeAdequateOptions = [ - ("No buildings", "When the built building count is not specified, behave as if it was set to 0."), - ("Enough buildings", "When the built building count is not specified, behave as if it matches the required building count.")]; + private static readonly (LocalizableString0, LocalizableString0?)[] displayStateOptionKeys = [ + (LSs.ShoppingListTotalBuildings, LSs.ShoppingListTotalBuildingsHint), + (LSs.ShoppingListBuiltBuildings, LSs.ShoppingListBuiltBuildingsHint), + (LSs.ShoppingListMissingBuildings, LSs.ShoppingListMissingBuildingsHint)]; + private static readonly (LocalizableString0, LocalizableString0?)[] assumeAdequateOptionKeys = [ + (LSs.ShoppingListAssumeNoBuildings, LSs.ShoppingListAssumeNoBuildingsHint), + (LSs.ShoppingListAssumeEnoughBuildings, LSs.ShoppingListAssumeEnoughBuildingsHint)]; public override void Build(ImGui gui) { - BuildHeader(gui, "Shopping list"); - gui.BuildText( - "Total cost of all objects: ¥" + DataUtils.FormatAmount(shoppingCost, UnitOfMeasure.None) + ", buildings: " + - DataUtils.FormatAmount(totalBuildings, UnitOfMeasure.None) + ", modules: " + DataUtils.FormatAmount(totalModules, UnitOfMeasure.None), TextBlockDisplayStyle.Centered); + BuildHeader(gui, LSs.ShoppingList); + gui.BuildText(LSs.ShoppingListCostInformation.L(DataUtils.FormatAmount(shoppingCost, UnitOfMeasure.None), + DataUtils.FormatAmount(totalBuildings, UnitOfMeasure.None), DataUtils.FormatAmount(totalModules, UnitOfMeasure.None)), + TextBlockDisplayStyle.Centered); using (gui.EnterRow()) { - if (gui.BuildRadioGroup(displayStateOptions, (int)displayState, out int newSelected)) { + if (gui.BuildRadioGroup(displayStateOptionKeys, (int)displayState, out int newSelected)) { displayState = (DisplayState)newSelected; RebuildData(); } } using (gui.EnterRow()) { SchemeColor textColor = displayState == DisplayState.Total ? SchemeColor.PrimaryTextFaint : SchemeColor.PrimaryText; - gui.BuildText("When not specified, assume:", TextBlockDisplayStyle.Default(textColor), topOffset: .15f); - if (gui.BuildRadioGroup(assumeAdequateOptions, assumeAdequate ? 1 : 0, out int newSelected, enabled: displayState != DisplayState.Total)) { + gui.BuildText(LSs.ShoppingListBuildingAssumptionHeader, TextBlockDisplayStyle.Default(textColor), topOffset: .15f); + if (gui.BuildRadioGroup(assumeAdequateOptionKeys, assumeAdequate ? 1 : 0, out int newSelected, enabled: displayState != DisplayState.Total)) { assumeAdequate = newSelected == 1; RebuildData(); } @@ -142,40 +143,40 @@ public override void Build(ImGui gui) { if (totalHeat > 0) { using (gui.EnterRow(0)) { gui.AllocateRect(0, 1.5f); - gui.BuildText("These entities require " + DataUtils.FormatAmount(totalHeat, UnitOfMeasure.Megawatt) + " heat on cold planets."); + gui.BuildText(LSs.ShoppingListTheseRequireHeat.L(DataUtils.FormatAmount(totalHeat, UnitOfMeasure.Megawatt))); } using (gui.EnterRow(0)) { - gui.BuildText("Allow additional heat for "); - if (gui.BuildLink("inserters")) { + gui.BuildText(LSs.ShoppingListAllowAdditionalHeat); + if (gui.BuildLink(LSs.ShoppingListHeatForInserters)) { gui.BuildObjectSelectDropDown(Database.allInserters, _ => { }, options); } - gui.BuildText(", "); - if (gui.BuildLink("pipes")) { + gui.BuildText(LSs.ListSeparator); + if (gui.BuildLink(LSs.ShoppingListHeatForPipes)) { gui.BuildObjectSelectDropDown(pipes, _ => { }, options); } - gui.BuildText(", "); - if (gui.BuildLink("belts")) { + gui.BuildText(LSs.ListSeparator); + if (gui.BuildLink(LSs.ShoppingListHeatForBelts)) { gui.BuildObjectSelectDropDown(belts, _ => { }, options); } - gui.BuildText(", and "); - if (gui.BuildLink("other entities")) { + gui.BuildText(LSs.ShoppingListHeatAnd); + if (gui.BuildLink(LSs.ShoppingListHeatForOtherEntities)) { gui.BuildObjectSelectDropDown(other, _ => { }, options); } - gui.BuildText("."); + gui.BuildText(LSs.ShoppingListHeatPeriod); } } using (gui.EnterRow(allocator: RectAllocator.RightRow)) { - if (gui.BuildButton("Done")) { + if (gui.BuildButton(LSs.Done)) { Close(); } - if (gui.BuildButton("Decompose", active: !decomposed)) { + if (gui.BuildButton(LSs.ShoppingListDecompose, active: !decomposed)) { Decompose(); } - if (gui.BuildButton("Export to blueprint", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.ShoppingListExportBlueprint, SchemeColor.Grey)) { gui.ShowDropDown(ExportBlueprintDropdown); } } @@ -201,16 +202,16 @@ public override void Build(ImGui gui) { } private void ExportBlueprintDropdown(ImGui gui) { - gui.BuildText("Blueprint string will be copied to clipboard", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ShoppingListExportBlueprintHint, TextBlockDisplayStyle.WrappedText); if (Database.objectsByTypeName.TryGetValue("Entity.constant-combinator", out var combinator) && gui.BuildFactorioObjectButtonWithText(combinator) == Click.Left && gui.CloseDropdown()) { - _ = BlueprintUtilities.ExportConstantCombinators("Shopping list", ExportGoods()); + _ = BlueprintUtilities.ExportConstantCombinators(LSs.ShoppingList, ExportGoods()); } foreach (var container in Database.allContainers) { if (container.logisticMode == "requester" && gui.BuildFactorioObjectButtonWithText(container) == Click.Left && gui.CloseDropdown()) { - _ = BlueprintUtilities.ExportRequesterChests("Shopping list", ExportGoods(), container); + _ = BlueprintUtilities.ExportRequesterChests(LSs.ShoppingList, ExportGoods(), container); } } } diff --git a/Yafc/Windows/WelcomeScreen.cs b/Yafc/Windows/WelcomeScreen.cs index e5498d0b..1eb2fc96 100644 --- a/Yafc/Windows/WelcomeScreen.cs +++ b/Yafc/Windows/WelcomeScreen.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json.Linq; using SDL2; using Serilog; +using Yafc.I18n; using Yafc.Model; using Yafc.Parser; using Yafc.UI; @@ -126,7 +127,7 @@ public WelcomeScreen(ProjectDefinition? cliProject = null) : base(ImGuiUtils.Def recentProjectScroll = new ScrollArea(20f, BuildRecentProjectList, collapsible: true); languageScroll = new ScrollArea(20f, LanguageSelection, collapsible: true); errorScroll = new ScrollArea(20f, BuildError, collapsible: true); - Create("Welcome to YAFC CE v" + YafcLib.version.ToString(3), 45, null); + Create(LSs.Welcome.L(YafcLib.version.ToString(3)), 45, null); if (cliProject != null && !string.IsNullOrEmpty(cliProject.dataPath)) { SetProject(cliProject); @@ -141,7 +142,7 @@ public WelcomeScreen(ProjectDefinition? cliProject = null) : base(ImGuiUtils.Def private void BuildError(ImGui gui) { if (errorMod != null) { - gui.BuildText($"Error while loading mod {errorMod}.", TextBlockDisplayStyle.Centered with { Color = SchemeColor.Error }); + gui.BuildText(LSs.ErrorWhileLoadingMod.L(errorMod), TextBlockDisplayStyle.Centered with { Color = SchemeColor.Error }); } gui.allocator = RectAllocator.Stretch; @@ -151,7 +152,7 @@ private void BuildError(ImGui gui) { protected override void BuildContents(ImGui gui) { gui.spacing = 1.5f; - gui.BuildText("Yet Another Factorio Calculator", new TextBlockDisplayStyle(Font.header, Alignment: RectAlignment.Middle)); + gui.BuildText(LSs.FullName, new TextBlockDisplayStyle(Font.header, Alignment: RectAlignment.Middle)); if (loading) { gui.BuildText(currentLoad1, TextBlockDisplayStyle.Centered); gui.BuildText(currentLoad2, TextBlockDisplayStyle.Centered); @@ -160,8 +161,8 @@ protected override void BuildContents(ImGui gui) { gui.SetNextRebuild(Ui.time + 30); } else if (downloading) { - gui.BuildText("Please wait . . .", TextBlockDisplayStyle.Centered); - gui.BuildText("Yafc is downloading the fonts for your language.", TextBlockDisplayStyle.Centered); + gui.BuildText(LSs.PleaseWait, TextBlockDisplayStyle.Centered); + gui.BuildText(LSs.DownloadingFonts, TextBlockDisplayStyle.Centered); } else if (errorMessage != null) { errorScroll.Build(gui); @@ -169,41 +170,40 @@ protected override void BuildContents(ImGui gui) { using (gui.EnterRow()) { if (thereIsAModToDisable) { - gui.BuildWrappedText("YAFC was unable to load the project. You can disable the problematic mod once by clicking on 'Disable & reload' button, or you can disable it " + - "permanently for YAFC by copying the mod-folder, disabling the mod in the copy by editing mod-list.json, and pointing YAFC to the copy."); + gui.BuildWrappedText(LSs.UnableToLoadWithMod); } else { - gui.BuildWrappedText("YAFC cannot proceed because it was unable to load the project."); + gui.BuildWrappedText(LSs.UnableToLoad); } } using (gui.EnterRow()) { - if (gui.BuildLink("More info")) { + if (gui.BuildLink(LSs.MoreInfo)) { ShowDropDown(gui, gui.lastRect, ProjectErrorMoreInfo, new Padding(0.5f), 30f); } } using (gui.EnterRow()) { - if (gui.BuildButton("Copy to clipboard", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.CopyToClipboard, SchemeColor.Grey)) { _ = SDL.SDL_SetClipboardText(errorMessage); } - if (thereIsAModToDisable && gui.BuildButton("Disable & reload").WithTooltip(gui, "Disable this mod until you close YAFC or change the mod folder.")) { + if (thereIsAModToDisable && gui.BuildButton(LSs.DisableAndReload).WithTooltip(gui, LSs.DisableAndReloadHint)) { FactorioDataSource.DisableMod(errorMod!); errorMessage = null; LoadProject(); } - if (gui.RemainingRow().BuildButton("Back")) { + if (gui.RemainingRow().BuildButton(LSs.BackButton)) { errorMessage = null; Rebuild(); } } } else { - BuildPathSelect(gui, path, "Project file location", "You can leave it empty for a new project", EditType.Workspace); - BuildPathSelect(gui, dataPath, "Factorio Data location*\nIt should contain folders 'base' and 'core'", - "e.g. C:/Games/Steam/SteamApps/common/Factorio/data", EditType.Factorio); - BuildPathSelect(gui, modsPath, "Factorio Mods location (optional)\nIt should contain file 'mod-list.json'", - "If you don't use separate mod folder, leave it empty", EditType.Mods); + BuildPathSelect(gui, path, LSs.WelcomeProjectFileLocation, LSs.WelcomeProjectFileLocationHint, EditType.Workspace); + BuildPathSelect(gui, dataPath, LSs.WelcomeDataLocation, + LSs.WelcomeDataLocationHint, EditType.Factorio); + BuildPathSelect(gui, modsPath, LSs.WelcomeModLocation, + LSs.WelcomeModLocationHint, EditType.Mods); using (gui.EnterRow()) { gui.allocator = RectAllocator.RightRow; @@ -216,11 +216,11 @@ protected override void BuildContents(ImGui gui) { gui.ShowDropDown(languageScroll.Build); } - gui.BuildText("In-game objects language:"); + gui.BuildText(LSs.WelcomeLanguageHeader); } using (gui.EnterRowWithHelpIcon("""When enabled it will try to find a more recent autosave. Disable if you want to load your manual save only.""", false)) { - if (gui.BuildCheckBox("Load most recent (auto-)save", Preferences.Instance.useMostRecentSave, + if (gui.BuildCheckBox(LSs.WelcomeLoadAutosave, Preferences.Instance.useMostRecentSave, out useMostRecentSave)) { Preferences.Instance.useMostRecentSave = useMostRecentSave; Preferences.Instance.Save(); @@ -231,16 +231,14 @@ protected override void BuildContents(ImGui gui) { If checked, YAFC will only suggest production or consumption recipes that have a net production or consumption of that item or fluid. For example, kovarex enrichment will not be suggested when adding recipes that produce U-238 or consume U-235. """, false)) { - _ = gui.BuildCheckBox("Use net production/consumption when analyzing recipes", netProduction, out netProduction); + _ = gui.BuildCheckBox(LSs.WelcomeUseNetProduction, netProduction, out netProduction); } - string softwareRenderHint = "If checked, the main project screen will not use hardware-accelerated rendering.\n\n" + - "Enable this setting if YAFC crashes after loading without an error message, or if you know that your computer's " + - "graphics hardware does not support modern APIs (e.g. DirectX 12 on Windows)."; + string softwareRenderHint = LSs.WelcomeSoftwareRenderHint; using (gui.EnterRowWithHelpIcon(softwareRenderHint, false)) { bool forceSoftwareRenderer = Preferences.Instance.forceSoftwareRenderer; - _ = gui.BuildCheckBox("Force software rendering in project screen", forceSoftwareRenderer, out forceSoftwareRenderer); + _ = gui.BuildCheckBox(LSs.WelcomeSoftwareRender, forceSoftwareRenderer, out forceSoftwareRenderer); if (forceSoftwareRenderer != Preferences.Instance.forceSoftwareRenderer) { Preferences.Instance.forceSoftwareRenderer = forceSoftwareRenderer; @@ -250,15 +248,15 @@ protected override void BuildContents(ImGui gui) { using (gui.EnterRow()) { if (Preferences.Instance.recentProjects.Length > 1) { - if (gui.BuildButton("Recent projects", SchemeColor.Grey)) { + if (gui.BuildButton(LSs.RecentProjects, SchemeColor.Grey)) { gui.ShowDropDown(BuildRecentProjectsDropdown, 35f); } } - if (gui.BuildButton(Icon.Help).WithTooltip(gui, "About YAFC")) { + if (gui.BuildButton(Icon.Help).WithTooltip(gui, LSs.AboutYafc)) { _ = new AboutScreen(this); } - if (gui.BuildButton(Icon.DarkMode).WithTooltip(gui, "Toggle dark mode")) { + if (gui.BuildButton(Icon.DarkMode).WithTooltip(gui, LSs.ToggleDarkMode)) { Preferences.Instance.darkMode = !Preferences.Instance.darkMode; RenderingUtils.SetColorScheme(Preferences.Instance.darkMode); Preferences.Instance.Save(); @@ -272,19 +270,15 @@ protected override void BuildContents(ImGui gui) { private void ProjectErrorMoreInfo(ImGui gui) { - gui.BuildWrappedText("Check that these mods load in Factorio."); - gui.BuildWrappedText("YAFC only supports loading mods that were loaded in Factorio before. If you add or remove mods or change startup settings, " + - "you need to load those in Factorio and then close the game because Factorio saves mod-list.json only when exiting."); - gui.BuildWrappedText("Check that Factorio loads mods from the same folder as YAFC."); - gui.BuildWrappedText("If that doesn't help, try removing the mods that have several versions, or are disabled, or don't have the required dependencies."); + gui.BuildWrappedText(LSs.LoadErrorAdvice); // The whole line is underlined if the allocator is not set to LeftAlign gui.allocator = RectAllocator.LeftAlign; - if (gui.BuildLink("If all else fails, then create an issue on GitHub")) { + if (gui.BuildLink(LSs.LoadErrorCreateIssue)) { Ui.VisitLink(AboutScreen.Github); } - gui.BuildWrappedText("Please attach a new-game save file to sync mods, versions, and settings."); + gui.BuildWrappedText(LSs.LoadErrorCreateIssueWithInformation); } private void DoLanguageList(ImGui gui, SortedList list, bool listFontSupported) { @@ -307,9 +301,9 @@ private void DoLanguageList(ImGui gui, SortedList list, bo } else { gui.ShowDropDown(async gui => { - gui.BuildText("Yafc will download a suitable font before it restarts.\nThis may take a minute or two.", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.WelcomeAlertDownloadFont, TextBlockDisplayStyle.WrappedText); gui.allocator = RectAllocator.Center; - if (gui.BuildButton("Confirm")) { + if (gui.BuildButton(LSs.Confirm)) { gui.CloseDropdown(); downloading = true; // Jump through several hoops to download an appropriate Noto Sans font. @@ -364,37 +358,36 @@ static void restartIfNecessary() { private void LanguageSelection(ImGui gui) { gui.spacing = 0f; gui.allocator = RectAllocator.LeftAlign; - gui.BuildText("Mods may not support your language, using English as a fallback.", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.WelcomeAlertEnglishFallback, TextBlockDisplayStyle.WrappedText); gui.AllocateSpacing(0.5f); DoLanguageList(gui, languageMapping, true); if (!Program.hasOverriddenFont) { gui.AllocateSpacing(0.5f); - string unsupportedLanguageMessage = "These languages are not supported by the current font. Click the language to restart with a suitable font, or click 'Select font' to select a custom font."; - gui.BuildText(unsupportedLanguageMessage, TextBlockDisplayStyle.WrappedText); + gui.BuildText((string)LSs.WelcomeAlertNeedADifferentFont, TextBlockDisplayStyle.WrappedText); gui.AllocateSpacing(0.5f); } DoLanguageList(gui, languageMapping, false); gui.AllocateSpacing(0.5f); - if (gui.BuildButton("Select font")) { + if (gui.BuildButton(LSs.WelcomeSelectFont)) { SelectFont(); } if (Preferences.Instance.overrideFont != null) { gui.BuildText(Preferences.Instance.overrideFont, TextBlockDisplayStyle.WrappedText); - if (gui.BuildLink("Reset font to default")) { + if (gui.BuildLink(LSs.WelcomeResetFont)) { Preferences.Instance.overrideFont = null; languageScroll.RebuildContents(); Preferences.Instance.Save(); } } - gui.BuildText("Restart Yafc to switch to the selected font.", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.WelcomeResetFontRestart, TextBlockDisplayStyle.WrappedText); } private async void SelectFont() { - string? result = await new FilesystemScreen("Override font", "Override font that YAFC uses", "Ok", null, FilesystemScreen.Mode.SelectFile, null, this, null, null); + string? result = await new FilesystemScreen(LSs.OverrideFont, LSs.OverrideFontLong, LSs.Ok, null, FilesystemScreen.Mode.SelectFile, null, this, null, null); if (result == null) { return; } @@ -419,19 +412,19 @@ private void ValidateSelection() { bool projectExists = File.Exists(path); if (projectExists) { - createText = "Load '" + Path.GetFileNameWithoutExtension(path) + "'"; + createText = LSs.LoadProjectName.L(Path.GetFileNameWithoutExtension(path)); } else if (path != "") { string? directory = Path.GetDirectoryName(path); if (!Directory.Exists(directory)) { - createText = "Project directory does not exist"; + createText = LSs.WelcomeAlertMissingDirectory; canCreate = false; return; } - createText = "Create '" + Path.GetFileNameWithoutExtension(path) + "'"; + createText = LSs.WelcomeCreateProjectName.L(Path.GetFileNameWithoutExtension(path)); } else { - createText = "Create new project"; + createText = LSs.WelcomeCreateUnnamedProject; } canCreate = factorioValid && modsValid; @@ -441,7 +434,7 @@ private void BuildPathSelect(ImGui gui, string path, string description, string gui.BuildText(description, TextBlockDisplayStyle.WrappedText); gui.spacing = 0.5f; using (gui.EnterGroup(default, RectAllocator.RightRow)) { - if (gui.BuildButton("...")) { + if (gui.BuildButton(LSs.WelcomeBrowseButton)) { ShowFileSelect(description, path, editType); } @@ -558,19 +551,19 @@ private async void ShowFileSelect(string description, string path, EditType type FilesystemScreen.Mode fsMode; if (type == EditType.Workspace) { - buttonText = "Select"; + buttonText = LSs.Select; location = Path.GetDirectoryName(path); fsMode = FilesystemScreen.Mode.SelectOrCreateFile; fileExtension = "yafc"; } else { - buttonText = "Select folder"; + buttonText = LSs.SelectFolder; location = path; fsMode = FilesystemScreen.Mode.SelectFolder; fileExtension = null; } - string? result = await new FilesystemScreen("Select folder", description, buttonText, location, fsMode, "", this, GetFolderFilter(type), fileExtension); + string? result = await new FilesystemScreen(LSs.SelectFolder, description, buttonText, location, fsMode, "", this, GetFolderFilter(type), fileExtension); if (result != null) { if (type == EditType.Factorio) { diff --git a/Yafc/Windows/WizardPanel.cs b/Yafc/Windows/WizardPanel.cs index 7c744144..6aa16a99 100644 --- a/Yafc/Windows/WizardPanel.cs +++ b/Yafc/Windows/WizardPanel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Yafc.I18n; using Yafc.UI; #nullable disable warnings // Disabling nullable for legacy code. @@ -33,7 +34,7 @@ public override void Build(ImGui gui) { bool valid = true; pages[page](gui, ref valid); using (gui.EnterRow(allocator: RectAllocator.RightRow)) { - if (gui.BuildButton(page >= pages.Count - 1 ? "Finish" : "Next", active: valid)) { + if (gui.BuildButton(page >= pages.Count - 1 ? LSs.WizardFinish : LSs.WizardNext, active: valid)) { if (page < pages.Count - 1) { page++; } @@ -42,11 +43,11 @@ public override void Build(ImGui gui) { finish(); } } - if (page > 0 && gui.BuildButton("Previous")) { + if (page > 0 && gui.BuildButton(LSs.WizardPrevious)) { page--; } - gui.BuildText("Step " + (page + 1) + " of " + pages.Count); + gui.BuildText(LSs.WizardStepXOfY.L((page + 1), pages.Count)); } } } diff --git a/Yafc/Workspace/AutoPlannerView.cs b/Yafc/Workspace/AutoPlannerView.cs index 64fd0f7c..a95f8d99 100644 --- a/Yafc/Workspace/AutoPlannerView.cs +++ b/Yafc/Workspace/AutoPlannerView.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -25,15 +26,15 @@ protected override void BuildPageTooltip(ImGui gui, AutoPlanner contents) { private Action CreateAutoPlannerWizard(List pages) { List goal = []; - string pageName = "Auto planner"; + string pageName = LSs.AutoPlanner; void page1(ImGui gui, ref bool valid) { - gui.BuildText("This is an experimental feature and may lack functionality. Unfortunately, after some prototyping it wasn't very useful to work with. More research required.", + gui.BuildText(LSs.AutoPlannerWarning, TextBlockDisplayStyle.ErrorText); - gui.BuildText("Enter page name:"); + gui.BuildText(LSs.AutoPlannerPageName); _ = gui.BuildTextInput(pageName, out pageName, null); gui.AllocateSpacing(2f); - gui.BuildText("Select your goal:"); + gui.BuildText(LSs.AutoPlannerGoal); using (var grid = gui.EnterInlineGrid(3f)) { for (int i = 0; i < goal.Count; i++) { var elem = goal[i]; @@ -50,7 +51,7 @@ void page1(ImGui gui, ref bool valid) { } grid.Next(); if (gui.BuildButton(Icon.Plus, SchemeColor.Primary, SchemeColor.PrimaryAlt, size: 2.5f)) { - SelectSingleObjectPanel.Select(Database.goods.explorable, "New production goal", x => { + SelectSingleObjectPanel.Select(Database.goods.explorable, LSs.AutoPlannerSelectProductionGoal, x => { goal.Add(new AutoPlannerGoal { amount = 1f, item = x }); gui.Rebuild(); }); @@ -58,7 +59,7 @@ void page1(ImGui gui, ref bool valid) { grid.Next(); } gui.AllocateSpacing(2f); - gui.BuildText("Review active milestones, as they will restrict recipes that are considered:", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.AutoPlannerReviewMilestones, TextBlockDisplayStyle.WrappedText); new MilestonesWidget().Build(gui); gui.AllocateSpacing(2f); valid = !string.IsNullOrEmpty(pageName) && goal.Count > 0; @@ -66,7 +67,7 @@ void page1(ImGui gui, ref bool valid) { pages.Add(page1); return () => { - var planner = MainScreen.Instance.AddProjectPage("Auto planner", goal[0].item, typeof(AutoPlanner), false, false); + var planner = MainScreen.Instance.AddProjectPage(LSs.AutoPlanner, goal[0].item, typeof(AutoPlanner), false, false); (planner.content as AutoPlanner).goals.AddRange(goal); MainScreen.Instance.SetActivePage(planner); }; diff --git a/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs b/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs index 6a799cc6..bc50311d 100644 --- a/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs +++ b/Yafc/Workspace/ProductionSummary/ProductionSummaryView.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -20,7 +21,7 @@ public ProductionSummaryView() { firstColumn = new SummaryColumn(this); lastColumn = new RestGoodsColumn(this); grid = new DataGrid(padding, firstColumn, lastColumn) { headerHeight = 4.2f }; - flatHierarchy = new FlatHierarchy(grid, null, buildExpandedGroupRows: true); + flatHierarchy = new FlatHierarchy(grid, null, LSs.LegacySummaryEmptyGroup, buildExpandedGroupRows: true); } private class PaddingColumn(ProductionSummaryView view) : DataColumn(3f) { @@ -79,7 +80,7 @@ public override void BuildElement(ImGui gui, ProductionSummaryEntry entry) { BuildButtons(gui, 1.5f, entry.subgroup); } else { - if (gui.BuildTextInput(entry.subgroup.name, out string newText, "Group name", delayed: true)) { + if (gui.BuildTextInput(entry.subgroup.name, out string newText, LSs.LegacySummaryGroupNameHint, delayed: true)) { entry.subgroup.RecordUndo().name = newText; } } @@ -100,11 +101,11 @@ public override void BuildElement(ImGui gui, ProductionSummaryEntry entry) { } else if (buttonEvent == ButtonEvent.Click) { gui.ShowDropDown(dropdownGui => { - if (dropdownGui.BuildButton("Go to page") && dropdownGui.CloseDropdown()) { + if (dropdownGui.BuildButton(LSs.LegacySummaryGoToPage) && dropdownGui.CloseDropdown()) { MainScreen.Instance.SetActivePage(entry.page.page); } - if (dropdownGui.BuildRedButton("Remove") && dropdownGui.CloseDropdown()) { + if (dropdownGui.BuildRedButton(LSs.Remove) && dropdownGui.CloseDropdown()) { _ = entry.owner.RecordUndo().elements.Remove(entry); } }); @@ -113,7 +114,7 @@ public override void BuildElement(ImGui gui, ProductionSummaryEntry entry) { using (gui.EnterFixedPositioning(3f, 2f, default)) { gui.allocator = RectAllocator.LeftRow; - gui.BuildText("x"); + gui.BuildText(LSs.LegacySummaryMultiplierEditBoxPrefix); DisplayAmount amount = entry.multiplier; if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.FactorioObjectInput with { ColorGroup = SchemeColorGroup.Grey, Alignment = RectAlignment.MiddleLeft }) && amount.Value >= 0) { @@ -178,7 +179,7 @@ public override void BuildElement(ImGui gui, ProductionSummaryEntry data) { } } - private class RestGoodsColumn(ProductionSummaryView view) : TextDataColumn("Other", 30f, 5f, 40f) { + private class RestGoodsColumn(ProductionSummaryView view) : TextDataColumn(LSs.LegacySummaryOtherColumn, 30f, 5f, 40f) { public override void BuildElement(ImGui gui, ProductionSummaryEntry data) { using var grid = gui.EnterInlineGrid(2.1f); foreach (var (goods, amount) in data.flow) { @@ -281,10 +282,10 @@ protected override void BuildContent(ImGui gui) { gui.AllocateSpacing(1f); using (gui.EnterGroup(new Padding(1))) { if (model.group.elements.Count == 0) { - gui.BuildText("Add your existing sheets here to keep track of what you have in your base and to see what shortages you may have"); + gui.BuildText(LSs.LegacySummaryEmptyGroupDescription); } else { - gui.BuildText("List of goods produced/consumed by added blocks. Click on any of these to add it to (or remove it from) the table."); + gui.BuildText(LSs.LegacySummaryGroupDescription); } using var inlineGrid = gui.EnterInlineGrid(3f, 1f); diff --git a/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs b/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs index 69c7fa2c..9d93218d 100644 --- a/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs +++ b/Yafc/Workspace/ProductionTable/ModuleCustomizationScreen.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -38,22 +39,22 @@ public static void Show(ProjectModuleTemplate template) { } public override void Build(ImGui gui) { - BuildHeader(gui, "Module customization"); + BuildHeader(gui, LSs.ModuleCustomization); if (template != null) { using (gui.EnterRow()) { if (gui.BuildFactorioObjectButton(template.icon, ButtonDisplayStyle.Default) == Click.Left) { - SelectSingleObjectPanel.SelectWithNone(Database.objects.all, "Select icon", x => { + SelectSingleObjectPanel.SelectWithNone(Database.objects.all, LSs.SelectIcon, x => { template.RecordUndo().icon = x; Rebuild(); }); } - if (gui.BuildTextInput(template.name, out string newName, "Enter name", delayed: true) && newName != "") { + if (gui.BuildTextInput(template.name, out string newName, LSs.ModuleCustomizationNameHint, delayed: true) && newName != "") { template.RecordUndo().name = newName; } } - gui.BuildText("Filter by crafting buildings (Optional):"); + gui.BuildText(LSs.ModuleCustomizationFilterBuildings); using var grid = gui.EnterInlineGrid(2f, 1f); for (int i = 0; i < template.filterEntities.Count; i++) { @@ -78,32 +79,32 @@ void doToSelectedItem(EntityCrafter selectedCrafter) { } SelectSingleObjectPanel.Select(Database.allCrafters.Where(isSuitable), - "Add module template filter", + LSs.ModuleCustomizationAddFilterBuilding, doToSelectedItem); } } if (modules == null) { - if (gui.BuildButton("Enable custom modules")) { + if (gui.BuildButton(LSs.ModuleCustomizationEnable)) { modules = new ModuleTemplateBuilder(); } } else { ModuleEffects effects = new ModuleEffects(); if (recipe == null || recipe.entity?.target.moduleSlots > 0) { - gui.BuildText("Internal modules:", Font.subheader); - gui.BuildText("Leave zero amount to fill the remaining slots"); + gui.BuildText(LSs.ModuleCustomizationInternalModules, Font.subheader); + gui.BuildText(LSs.ModuleCustomizationLeaveZeroHint); DrawRecipeModules(gui, null, ref effects); } else { - gui.BuildText("This building doesn't have module slots, but can be affected by beacons"); + gui.BuildText(LSs.ModuleCustomizationBeaconsOnly); } - gui.BuildText("Beacon modules:", Font.subheader); + gui.BuildText(LSs.ModuleCustomizationBeaconModules, Font.subheader); if (modules.beacon == null) { - gui.BuildText("Use default parameters"); - if (gui.BuildButton("Override beacons as well")) { + gui.BuildText(LSs.ModuleCustomizationUsingDefaultBeacons); + if (gui.BuildButton(LSs.ModuleCustomizationOverrideBeacons)) { SelectBeacon(gui); } @@ -118,7 +119,7 @@ void doToSelectedItem(EntityCrafter selectedCrafter) { SelectBeacon(gui); } - string modulesNotBeacons = "Input the amount of modules, not the amount of beacons. Single beacon can hold " + modules.beacon.target.moduleSlots + " modules."; + string modulesNotBeacons = LSs.ModuleCustomizationUseNumberOfModulesInBeacons.L(modules.beacon.target.moduleSlots); gui.BuildText(modulesNotBeacons, TextBlockDisplayStyle.WrappedText); DrawRecipeModules(gui, modules.beacon, ref effects); } @@ -142,25 +143,25 @@ void doToSelectedItem(EntityCrafter selectedCrafter) { if (recipe != null) { float craftingSpeed = (recipe.entity?.GetCraftingSpeed() ?? 1f) * effects.speedMod; - gui.BuildText("Current effects:", Font.subheader); - gui.BuildText("Productivity bonus: " + DataUtils.FormatAmount(effects.productivity, UnitOfMeasure.Percent)); - gui.BuildText("Speed bonus: " + DataUtils.FormatAmount(effects.speedMod - 1, UnitOfMeasure.Percent) + " (Crafting speed: " + - DataUtils.FormatAmount(craftingSpeed, UnitOfMeasure.None) + ")"); - gui.BuildText("Quality bonus: " + DataUtils.FormatAmount(effects.qualityMod, UnitOfMeasure.Percent) + " (multiplied by quality upgrade chance)"); + gui.BuildText(LSs.ModuleCustomizationCurrentEffects, Font.subheader); + gui.BuildText(LSs.ModuleCustomizationProductivityBonus.L(DataUtils.FormatAmount(effects.productivity, UnitOfMeasure.Percent))); + gui.BuildText(LSs.ModuleCustomizationSpeedBonus.L(DataUtils.FormatAmount(effects.speedMod - 1, UnitOfMeasure.Percent), DataUtils.FormatAmount(craftingSpeed, UnitOfMeasure.None))); + gui.BuildText(LSs.ModuleCustomizationQualityBonus.L(DataUtils.FormatAmount(effects.qualityMod, UnitOfMeasure.Percent))); - string energyUsageLine = "Energy usage: " + DataUtils.FormatAmount(effects.energyUsageMod, UnitOfMeasure.Percent); + string energyUsageLine = LSs.ModuleCustomizationEnergyUsage.L(DataUtils.FormatAmount(effects.energyUsageMod, UnitOfMeasure.Percent)); if (recipe.entity != null) { float power = effects.energyUsageMod * recipe.entity.GetPower() / recipe.entity.target.energy.effectivity; if (!recipe.recipe.target.flags.HasFlagAny(RecipeFlags.UsesFluidTemperature | RecipeFlags.ScaleProductionWithPower) && recipe.entity != null) { - energyUsageLine += " (" + DataUtils.FormatAmount(power, UnitOfMeasure.Megawatt) + " per building)"; + energyUsageLine = LSs.ModuleCustomizationEnergyUsagePerBuilding.L(DataUtils.FormatAmount(effects.energyUsageMod, UnitOfMeasure.Percent), + DataUtils.FormatAmount(power, UnitOfMeasure.Megawatt)); } gui.BuildText(energyUsageLine); float pps = craftingSpeed * (1f + MathF.Max(0f, effects.productivity)) / recipe.recipe.target.time; - gui.BuildText("Overall crafting speed (including productivity): " + DataUtils.FormatAmount(pps, UnitOfMeasure.PerSecond)); - gui.BuildText("Energy cost per recipe output: " + DataUtils.FormatAmount(power / pps, UnitOfMeasure.Megajoule)); + gui.BuildText(LSs.ModuleCustomizationOverallSpeed.L(DataUtils.FormatAmount(pps, UnitOfMeasure.PerSecond))); + gui.BuildText(LSs.ModuleCustomizationEnergyCostPerOutput.L(DataUtils.FormatAmount(power / pps, UnitOfMeasure.Megajoule))); } else { gui.BuildText(energyUsageLine); @@ -170,18 +171,18 @@ void doToSelectedItem(EntityCrafter selectedCrafter) { gui.AllocateSpacing(3f); using (gui.EnterRow(allocator: RectAllocator.RightRow)) { - if (template == null && gui.BuildButton("Cancel")) { + if (template == null && gui.BuildButton(LSs.Cancel)) { Close(); } - if (template != null && gui.BuildButton("Cancel (partial)")) { + if (template != null && gui.BuildButton(LSs.PartialCancel)) { Close(); } - if (gui.BuildButton("Done")) { + if (gui.BuildButton(LSs.Done)) { CloseWithResult(modules); } gui.allocator = RectAllocator.LeftRow; - if (modules != null && recipe != null && gui.BuildRedButton("Remove module customization")) { + if (modules != null && recipe != null && gui.BuildRedButton(LSs.ModuleCustomizationRemove)) { CloseWithResult(null); } } @@ -192,13 +193,13 @@ private void SelectBeacon(ImGui gui) { gui.BuildObjectQualitySelectDropDown(Database.allBeacons, sel => { modules.beacon = sel; contents.Rebuild(); - }, new("Select beacon"), Quality.Normal); + }, new(LSs.SelectBeacon), Quality.Normal); } else { gui.BuildObjectQualitySelectDropDownWithNone(Database.allBeacons, sel => { modules.beacon = sel; contents.Rebuild(); - }, new("Select beacon"), modules.beacon.quality, quality => { + }, new(LSs.SelectBeacon), modules.beacon.quality, quality => { modules.beacon = modules.beacon.With(quality); contents.Rebuild(); }); @@ -237,7 +238,7 @@ private void DrawRecipeModules(ImGui gui, IObjectWithQuality? beac list[idx] = (sel, list[idx].fixedCount); } gui.Rebuild(); - }, new("Select module", DataUtils.FavoriteModule), list[idx].module.quality, quality => { + }, new(LSs.SelectModule, DataUtils.FavoriteModule), list[idx].module.quality, quality => { list[idx] = list[idx] with { module = list[idx].module.target.With(quality) }; gui.Rebuild(); }); @@ -266,7 +267,7 @@ private void DrawRecipeModules(ImGui gui, IObjectWithQuality? beac gui.BuildObjectQualitySelectDropDown(GetModules(beacon), sel => { list.Add(new(sel, 0)); gui.Rebuild(); - }, new("Select module", DataUtils.FavoriteModule), Quality.Normal); + }, new(LSs.SelectModule, DataUtils.FavoriteModule), Quality.Normal); } } diff --git a/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs b/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs index 9b660547..160dbc8d 100644 --- a/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs +++ b/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -31,11 +32,11 @@ private void ListDrawer(ImGui gui, KeyValuePair { using (gui.EnterRow()) { // Allocate the width now, but draw the text later so it can be vertically centered. - Rect rect = gui.AllocateTextRect(out _, "Affected by " + element.Value.beaconCount, TextBlockDisplayStyle.Default(SchemeColor.None)); + Rect rect = gui.AllocateTextRect(out _, LSs.AffectedByNBeacons.L(element.Value.beaconCount), TextBlockDisplayStyle.Default(SchemeColor.None)); gui.BuildFactorioObjectIcon(element.Value.beacon, ButtonDisplayStyle.ProductionTableUnscaled); rect.Height = gui.lastRect.Height; - gui.DrawText(rect, "Affected by " + element.Value.beaconCount); - gui.BuildText("each containing " + element.Value.beacon.target.moduleSlots); + gui.DrawText(rect, LSs.AffectedByNBeacons.L(element.Value.beaconCount)); + gui.BuildText(LSs.EachContainingNModules.L(element.Value.beacon.target.moduleSlots)); gui.BuildFactorioObjectIcon(element.Value.beaconModule, ButtonDisplayStyle.ProductionTableUnscaled); } } @@ -44,7 +45,7 @@ private void ListDrawer(ImGui gui, KeyValuePair { + SelectSingleObjectPanel.SelectQualityWithNone(Database.usableBeacons, LSs.SelectBeacon, selectedBeacon => { if (selectedBeacon is null) { _ = modules.overrideCrafterBeacons.Remove(crafter); @@ -60,11 +61,11 @@ private void ListDrawer(ImGui gui, KeyValuePair modules.overrideCrafterBeacons[crafter].beacon.target.CanAcceptModule(m.moduleSpecification)), - "Select beacon module", selectedModule => { + LSs.SelectBeaconModule, selectedModule => { if (selectedModule is null) { _ = modules.overrideCrafterBeacons.Remove(crafter); @@ -74,7 +75,7 @@ private void ListDrawer(ImGui gui, KeyValuePair= 0: modules.overrideCrafterBeacons[crafter] = modules.overrideCrafterBeacons[crafter] with { beaconCount = (int)amount.Value }; @@ -95,13 +96,13 @@ public static void BuildSimple(ImGui gui, ModuleFillerParameters modules) { } if (payback <= 0f) { - gui.BuildText("Use no modules"); + gui.BuildText(LSs.UseNoModules); } else if (payback >= float.MaxValue) { - gui.BuildText("Use best modules"); + gui.BuildText(LSs.UseBestModules); } else { - gui.BuildText("Modules payback estimate: " + DataUtils.FormatTime(payback), TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ModuleFillerPaybackEstimate.L(DataUtils.FormatTime(payback)), TextBlockDisplayStyle.WrappedText); } } @@ -114,27 +115,27 @@ public override void Build(ImGui gui) { IObjectWithQuality? defaultBeacon = beacon.With(Quality.MaxAccessible); IObjectWithQuality? beaconFillerModule = defaultBeaconModule.With(Quality.MaxAccessible); - BuildHeader(gui, "Module autofill parameters"); + BuildHeader(gui, LSs.ModuleFillerHeaderAutofill); BuildSimple(gui, modules); - if (gui.BuildCheckBox("Fill modules in miners", modules.fillMiners, out bool newFill)) { + if (gui.BuildCheckBox(LSs.ModuleFillerFillMiners, modules.fillMiners, out bool newFill)) { modules.fillMiners = newFill; } gui.AllocateSpacing(); - gui.BuildText("Filler module:", Font.subheader); - gui.BuildText("Use this module when autofill doesn't add anything (for example when productivity modules doesn't fit)", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ModuleFillerModule, Font.subheader); + gui.BuildText(LSs.ModuleFillerModuleHint, TextBlockDisplayStyle.WrappedText); if (gui.BuildFactorioObjectButtonWithText(modules.fillerModule) == Click.Left) { - SelectSingleObjectPanel.SelectQualityWithNone(Database.allModules, "Select filler module", select => modules.fillerModule = select, modules.fillerModule?.quality); + SelectSingleObjectPanel.SelectQualityWithNone(Database.allModules, LSs.ModuleFillerSelectModule, select => modules.fillerModule = select, modules.fillerModule?.quality); } gui.AllocateSpacing(); - gui.BuildText("Beacons & beacon modules:", Font.subheader); + gui.BuildText(LSs.ModuleFillerHeaderBeacons, Font.subheader); if (defaultBeacon is null || beaconFillerModule is null) { - gui.BuildText("Your mods contain no beacons, or no modules that can be put into beacons."); + gui.BuildText(LSs.ModuleFillerNoBeacons); } else { if (gui.BuildFactorioObjectButtonWithText(modules.beacon) == Click.Left) { - SelectSingleObjectPanel.SelectQualityWithNone(Database.allBeacons, "Select beacon", select => { + SelectSingleObjectPanel.SelectQualityWithNone(Database.allBeacons, LSs.SelectBeacon, select => { modules.beacon = select; if (modules.beaconModule != null && (modules.beacon == null || !modules.beacon.target.CanAcceptModule(modules.beaconModule))) { modules.beaconModule = null; @@ -146,35 +147,34 @@ public override void Build(ImGui gui) { if (gui.BuildFactorioObjectButtonWithText(modules.beaconModule) == Click.Left) { SelectSingleObjectPanel.SelectQualityWithNone(Database.allModules.Where(x => modules.beacon?.target.CanAcceptModule(x.moduleSpecification) ?? false), - "Select module for beacon", select => modules.beaconModule = select, modules.beaconModule?.quality); + LSs.ModuleFillerSelectBeaconModule, select => modules.beaconModule = select, modules.beaconModule?.quality); } using (gui.EnterRow()) { - gui.BuildText("Beacons per building: "); + gui.BuildText(LSs.ModuleFillerBeaconsPerBuilding); DisplayAmount amount = modules.beaconsPerBuilding; if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.ModuleParametersTextInput) && (int)amount.Value > 0) { modules.beaconsPerBuilding = (int)amount.Value; } } - gui.BuildText("Please note that beacons themselves are not part of the calculation", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ModuleFillerBeaconsNotCalculated, TextBlockDisplayStyle.WrappedText); gui.AllocateSpacing(); - gui.BuildText("Override beacons:", Font.subheader); + gui.BuildText(LSs.ModuleFillerOverrideBeacons, Font.subheader); if (modules.overrideCrafterBeacons.Count > 0) { using (gui.EnterGroup(new Padding(1, 0, 0, 0))) { - gui.BuildText("Click to change beacon, right-click to change module", topOffset: -0.5f); - gui.BuildText("Select the 'none' item in either prompt to remove the override.", topOffset: -0.5f); + gui.BuildText(LSs.ModuleFillerOverrideBeaconsHint, TextBlockDisplayStyle.WrappedText, topOffset: -0.5f); } } gui.AllocateSpacing(.5f); overrideList.Build(gui); using (gui.EnterRow(allocator: RectAllocator.Center)) { - if (gui.BuildButton("Add an override for a building type")) { + if (gui.BuildButton(LSs.ModuleFillerAddBeaconOverride)) { SelectMultiObjectPanel.Select(Database.allCrafters.Where(x => x.allowedEffects != AllowedEffects.None && !modules.overrideCrafterBeacons.ContainsKey(x)), - "Add exception(s) for:", + LSs.ModuleFillerSelectOverriddenCrafter, crafter => { modules.overrideCrafterBeacons[crafter] = new BeaconOverrideConfiguration(modules.beacon ?? defaultBeacon, modules.beaconsPerBuilding, modules.beaconModule ?? beaconFillerModule); @@ -184,7 +184,7 @@ public override void Build(ImGui gui) { } } - if (gui.BuildButton("Done")) { + if (gui.BuildButton(LSs.Done)) { Close(); } diff --git a/Yafc/Workspace/ProductionTable/ModuleTemplateConfiguration.cs b/Yafc/Workspace/ProductionTable/ModuleTemplateConfiguration.cs index 5af883e9..b1b0ba00 100644 --- a/Yafc/Workspace/ProductionTable/ModuleTemplateConfiguration.cs +++ b/Yafc/Workspace/ProductionTable/ModuleTemplateConfiguration.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -55,7 +56,7 @@ public override void Activated() { } public override void Build(ImGui gui) { - BuildHeader(gui, "Module templates"); + BuildHeader(gui, LSs.ModuleTemplates); templateList.Build(gui); if (pageToDelete != null) { _ = Project.current.RecordUndo().sharedModuleTemplates.Remove(pageToDelete); @@ -63,7 +64,7 @@ public override void Build(ImGui gui) { pageToDelete = null; } using (gui.EnterRow(0.5f, RectAllocator.RightRow)) { - if (gui.BuildButton("Create", active: newPageName != "")) { + if (gui.BuildButton(LSs.Create, active: newPageName != "")) { ProjectModuleTemplate template = new(Project.current, newPageName); Project.current.RecordUndo().sharedModuleTemplates.Add(template); newPageName = ""; @@ -71,7 +72,7 @@ public override void Build(ImGui gui) { RefreshList(); } - _ = gui.RemainingRow().BuildTextInput(newPageName, out newPageName, "Create new template", setKeyboardFocus: SetKeyboardFocus.OnFirstPanelDraw); + _ = gui.RemainingRow().BuildTextInput(newPageName, out newPageName, LSs.CreateNewTemplateHint, setKeyboardFocus: SetKeyboardFocus.OnFirstPanelDraw); } } } diff --git a/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs b/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs index 9e83257b..0ca8007d 100644 --- a/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs +++ b/Yafc/Workspace/ProductionTable/ProductionLinkSummaryScreen.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using SDL2; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -24,27 +25,27 @@ private ProductionLinkSummaryScreen(ProductionLink link) { } private void BuildScrollArea(ImGui gui) { - gui.BuildText("Production: " + DataUtils.FormatAmount(totalInput, link.flowUnitOfMeasure), Font.subheader); + gui.BuildText(LSs.LinkSummaryProduction.L(DataUtils.FormatAmount(totalInput, link.flowUnitOfMeasure)), Font.subheader); BuildFlow(gui, input, totalInput, false); if (link.capturedRecipes.Any(r => r is not RecipeRow)) { // captured recipes that are not user-visible RecipeRows imply the existence of implicit links using (gui.EnterRow()) { gui.AllocateRect(0, ButtonDisplayStyle.Default.Size); - gui.BuildText("Plus additional production from implicit links"); + gui.BuildText(LSs.LinkSummaryImplicitLinks); } } gui.spacing = 0.5f; - gui.BuildText("Consumption: " + DataUtils.FormatAmount(totalOutput, link.flowUnitOfMeasure), Font.subheader); + gui.BuildText(LSs.LinkSummaryConsumption.L(DataUtils.FormatAmount(totalOutput, link.flowUnitOfMeasure)), Font.subheader); BuildFlow(gui, output, totalOutput, true); if (link.amount != 0) { gui.spacing = 0.5f; - gui.BuildText((link.amount > 0 ? "Requested production: " : "Requested consumption: ") + DataUtils.FormatAmount(MathF.Abs(link.amount), - link.flowUnitOfMeasure), new TextBlockDisplayStyle(Font.subheader, Color: SchemeColor.GreenAlt)); + gui.BuildText((link.amount > 0 ? LSs.LinkSummaryRequestedProduction : LSs.LinkSummaryRequestedConsumption).L(DataUtils.FormatAmount(MathF.Abs(link.amount), + link.flowUnitOfMeasure)), new TextBlockDisplayStyle(Font.subheader, Color: SchemeColor.GreenAlt)); } if (link.flags.HasFlags(ProductionLink.Flags.LinkNotMatched) && totalInput != totalOutput + link.amount) { float amount = totalInput - totalOutput - link.amount; gui.spacing = 0.5f; - gui.BuildText((amount > 0 ? "Overproduction: " : "Overconsumption: ") + DataUtils.FormatAmount(MathF.Abs(amount), link.flowUnitOfMeasure), + gui.BuildText((amount > 0 ? LSs.LinkSummaryOverproduction : LSs.LinkSummaryOverconsumption).L(DataUtils.FormatAmount(MathF.Abs(amount), link.flowUnitOfMeasure)), new TextBlockDisplayStyle(Font.subheader, Color: SchemeColor.Error)); } ShowRelatedLinks(gui); @@ -81,25 +82,25 @@ private void ShowRelatedLinks(ImGui gui) { var color = 1; gui.spacing = 0.75f; if (childLinks.Values.Any(e => e.Any())) { - gui.BuildText("Child links: ", Font.productionTableHeader); + gui.BuildText(LSs.LinkSummaryChildLinks, Font.productionTableHeader); foreach (var relTable in childLinks.Values) { BuildFlow(gui, relTable, relTable.Sum(e => Math.Abs(e.flow)), false, color++); } } if (parentLinks.Values.Any(e => e.Any())) { - gui.BuildText("Parent links: ", Font.productionTableHeader); + gui.BuildText(LSs.LinkSummaryParentLinks, Font.productionTableHeader); foreach (var relTable in parentLinks.Values) { BuildFlow(gui, relTable, relTable.Sum(e => Math.Abs(e.flow)), false, color++); } } if (otherLinks.Values.Any(e => e.Any())) { - gui.BuildText("Unrelated links: ", Font.productionTableHeader); + gui.BuildText(LSs.LinkSummaryUnrelatedLinks, Font.productionTableHeader); foreach (var relTable in otherLinks.Values) { BuildFlow(gui, relTable, relTable.Sum(e => Math.Abs(e.flow)), false, color++); } } if (unlinked.Any()) { - gui.BuildText("Unlinked: ", Font.subheader); + gui.BuildText(LSs.LinkSummaryUnlinked, Font.subheader); BuildFlow(gui, unlinked, 0, false); } } @@ -129,17 +130,17 @@ private bool IsLinkParent(RecipeRow row, List parents) => row.Ingre .Any(e => parents.Contains(e.owner)); public override void Build(ImGui gui) { - BuildHeader(gui, "Link summary"); + BuildHeader(gui, LSs.LinkSummary); using (gui.EnterRow()) { - gui.BuildText("Exploring link for: ", topOffset: 0.5f); - _ = gui.BuildFactorioObjectButtonWithText(link.goods, tooltipOptions: DrawParentRecipes(link.owner, "link")); + gui.BuildText(LSs.LinkSummaryHeader, topOffset: 0.5f); + _ = gui.BuildFactorioObjectButtonWithText(link.goods, tooltipOptions: DrawParentRecipes(link.owner, LSs.LinkSummaryLinkNestedUnder)); } scrollArea.Build(gui); - if (gui.BuildButton("Remove link", link.owner.allLinks.Contains(link) ? SchemeColor.Primary : SchemeColor.Grey)) { + if (gui.BuildButton(LSs.RemoveLink, link.owner.allLinks.Contains(link) ? SchemeColor.Primary : SchemeColor.Grey)) { DestroyLink(link); Close(); } - if (gui.BuildButton("Done")) { + if (gui.BuildButton(LSs.Done)) { Close(); } } @@ -155,7 +156,7 @@ private void BuildFlow(ImGui gui, List<(RecipeRow row, float flow)> list, float gui.spacing = 0f; foreach (var (row, flow) in list) { string amount = DataUtils.FormatAmount(flow, link.flowUnitOfMeasure); - if (gui.BuildFactorioObjectButtonWithText(row.recipe, amount, tooltipOptions: DrawParentRecipes(row.owner, "recipe")) == Click.Left) { + if (gui.BuildFactorioObjectButtonWithText(row.recipe, amount, tooltipOptions: DrawParentRecipes(row.owner, LSs.LinkSummaryRecipeNestedUnder)) == Click.Left) { // Find the corresponding links associated with the clicked recipe, IEnumerable> goods = (isLinkOutput ? row.Products.Select(p => p.Goods) : row.Ingredients.Select(i => i.Goods))!; Dictionary, ProductionLink> links = goods @@ -177,7 +178,7 @@ private void BuildFlow(ImGui gui, List<(RecipeRow row, float flow)> list, float void drawLinks(ImGui gui) { if (links.Count == 0) { - string text = isLinkOutput ? "This recipe has no linked products." : "This recipe has no linked ingredients"; + string text = isLinkOutput ? LSs.LinkSummaryNoProducts : LSs.LinkSummaryNoIngredients; gui.BuildText(text, TextBlockDisplayStyle.ErrorText); } else { @@ -186,7 +187,7 @@ void drawLinks(ImGui gui) { comparer = DataUtils.CustomFirstItemComparer(mainProduct, comparer); } - string header = isLinkOutput ? "Select product link to inspect" : "Select ingredient link to inspect"; + string header = isLinkOutput ? LSs.LinkSummarySelectProduct : LSs.LinkSummarySelectIngredient; ObjectSelectOptions> options = new(header, comparer, int.MaxValue); if (gui.BuildInlineObjectList(links.Keys, out IObjectWithQuality? selected, options) && gui.CloseDropdown()) { changeLinkView(links[selected]); @@ -228,7 +229,7 @@ private static SchemeColor GetFlowColor(int colorIndex) { /// The first table to consider when determining what recipe rows to report. /// "link" or "recipe", for the text "This X is nested under:" /// A that will draw the appropriate information when called. - private static DrawBelowHeader DrawParentRecipes(ProductionTable table, string type) => gui => { + private static DrawBelowHeader DrawParentRecipes(ProductionTable table, LocalizableString0 type) => gui => { // Collect the parent recipes (equivalently, the table headers of all tables that contain table) Stack parents = new(); while (table?.owner is RecipeRow row) { @@ -237,7 +238,7 @@ private static DrawBelowHeader DrawParentRecipes(ProductionTable table, string t } if (parents.Count > 0) { - gui.BuildText($"This {type} is nested under:", TextBlockDisplayStyle.WrappedText); + gui.BuildText(type, TextBlockDisplayStyle.WrappedText); // Draw the parents with nesting float padding = 0.5f; diff --git a/Yafc/Workspace/ProductionTable/ProductionTableFlatHierarchy.cs b/Yafc/Workspace/ProductionTable/ProductionTableFlatHierarchy.cs index 1eb1dc26..029467d3 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableFlatHierarchy.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableFlatHierarchy.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -17,7 +18,7 @@ namespace Yafc; /// GUI system. /// ///
-public class FlatHierarchy(DataGrid grid, Action? drawTableHeader, string emptyGroupMessage = "This is an empty group", bool buildExpandedGroupRows = true) +public class FlatHierarchy(DataGrid grid, Action? drawTableHeader, LocalizableString0 emptyGroupMessage, bool buildExpandedGroupRows = true) where TRow : ModelObject, IGroupedElement where TGroup : ModelObject, IElementGroup { // These two arrays contain: diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index ed8aadde..ad7aa01a 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -4,6 +4,7 @@ using System.Numerics; using SDL2; using Yafc.Blueprints; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -17,7 +18,7 @@ public ProductionTableView() { new IngredientsColumn(this), new ProductsColumn(this), new ModulesColumn(this)); flatHierarchyBuilder = new FlatHierarchy(grid, BuildSummary, - "This is a nested group. You can drag&drop recipes here. Nested groups can have their own linked materials."); + LSs.ProductionTableNestedGroup); } /// If not , names an instance property in that will be used to store the width of this column. @@ -67,9 +68,9 @@ public override void BuildElement(ImGui gui, RecipeRow row) { g.boxColor = SchemeColor.Error; g.textColor = SchemeColor.ErrorText; } - foreach (var (flag, text) in WarningsMeaning) { + foreach (var (flag, key) in WarningsMeaning) { if ((row.warningFlags & flag) != 0) { - g.BuildText(text, TextBlockDisplayStyle.WrappedText); + g.BuildText(key, TextBlockDisplayStyle.WrappedText); } } }); @@ -116,7 +117,7 @@ private static void BuildRowMarker(ImGui gui, RecipeRow row) { } } - private class RecipeColumn(ProductionTableView view) : ProductionTableDataColumn(view, "Recipe", 13f, 13f, 30f, widthStorage: nameof(Preferences.recipeColumnWidth)) { + private class RecipeColumn(ProductionTableView view) : ProductionTableDataColumn(view, LSs.ProductionTableHeaderRecipe, 13f, 13f, 30f, widthStorage: nameof(Preferences.recipeColumnWidth)) { public override void BuildElement(ImGui gui, RecipeRow recipe) { gui.spacing = 0.5f; switch (gui.BuildFactorioObjectButton(recipe.recipe, ButtonDisplayStyle.ProductionTableUnscaled)) { @@ -124,11 +125,11 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { gui.ShowDropDown(delegate (ImGui imgui) { DrawRecipeTagSelect(imgui, recipe); - if (recipe.subgroup == null && imgui.BuildButton("Create nested table") && imgui.CloseDropdown()) { + if (recipe.subgroup == null && imgui.BuildButton(LSs.ProductionTableCreateNested) && imgui.CloseDropdown()) { recipe.RecordUndo().subgroup = new ProductionTable(recipe); } - if (recipe.subgroup != null && imgui.BuildButton("Add nested desired product") && imgui.CloseDropdown()) { + if (recipe.subgroup != null && imgui.BuildButton(LSs.ProductionTableAddNestedProduct) && imgui.CloseDropdown()) { AddDesiredProductAtLevel(recipe.subgroup); } @@ -136,29 +137,29 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { BuildRecipeButton(imgui, recipe.subgroup); } - if (recipe.subgroup != null && imgui.BuildButton("Unpack nested table").WithTooltip(imgui, recipe.subgroup.expanded ? "Shortcut: right-click" : "Shortcut: Expand, then right-click") && imgui.CloseDropdown()) { + if (recipe.subgroup != null && imgui.BuildButton(LSs.ProductionTableUnpackNested).WithTooltip(imgui, recipe.subgroup.expanded ? LSs.ProductionTableShortcutRightClick : LSs.ProductionTableShortcutExpandAndRightClick) && imgui.CloseDropdown()) { unpackNestedTable(); } - if (recipe.subgroup != null && imgui.BuildButton("ShoppingList") && imgui.CloseDropdown()) { + if (recipe.subgroup != null && imgui.BuildButton(LSs.ShoppingList) && imgui.CloseDropdown()) { view.BuildShoppingList(recipe); } - if (imgui.BuildCheckBox("Show total Input/Output", recipe.showTotalIO, out bool newShowTotalIO)) { + if (imgui.BuildCheckBox(LSs.ProductionTableShowTotalIo, recipe.showTotalIO, out bool newShowTotalIO)) { recipe.RecordUndo().showTotalIO = newShowTotalIO; } - if (imgui.BuildCheckBox("Enabled", recipe.enabled, out bool newEnabled)) { + if (imgui.BuildCheckBox(LSs.Enabled, recipe.enabled, out bool newEnabled)) { recipe.RecordUndo().enabled = newEnabled; } - BuildFavorites(imgui, recipe.recipe.target, "Add recipe to favorites"); + BuildFavorites(imgui, recipe.recipe.target, LSs.AddRecipeToFavorites); - if (recipe.subgroup != null && imgui.BuildRedButton("Delete nested table").WithTooltip(imgui, recipe.subgroup.expanded ? "Shortcut: Collapse, then right-click" : "Shortcut: right-click") && imgui.CloseDropdown()) { + if (recipe.subgroup != null && imgui.BuildRedButton(LSs.ProductionTableDeleteNested).WithTooltip(imgui, recipe.subgroup.expanded ? LSs.ProductionTableShortcutCollapseAndRightClick : LSs.ProductionTableShortcutRightClick) && imgui.CloseDropdown()) { _ = recipe.owner.RecordUndo().recipes.Remove(recipe); } - if (recipe.subgroup == null && imgui.BuildRedButton("Delete recipe").WithTooltip(imgui, "Shortcut: right-click") && imgui.CloseDropdown()) { + if (recipe.subgroup == null && imgui.BuildRedButton(LSs.ProductionTableDeleteRecipe).WithTooltip(imgui, LSs.ProductionTableShortcutRightClick) && imgui.CloseDropdown()) { _ = recipe.owner.RecordUndo().recipes.Remove(recipe); } }); @@ -210,32 +211,32 @@ private static void RemoveZeroRecipes(ProductionTable productionTable) { public override void BuildMenu(ImGui gui) { BuildRecipeButton(gui, view.model); - gui.BuildText("Export inputs and outputs to blueprint with constant combinators:", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ProductionTableExportToBlueprint, TextBlockDisplayStyle.WrappedText); using (gui.EnterRow()) { - gui.BuildText("Amount per:"); + gui.BuildText(LSs.ExportBlueprintAmountPer); - if (gui.BuildLink("second") && gui.CloseDropdown()) { + if (gui.BuildLink(LSs.ExportBlueprintAmountPerSecond) && gui.CloseDropdown()) { ExportIo(1f); } - if (gui.BuildLink("minute") && gui.CloseDropdown()) { + if (gui.BuildLink(LSs.ExportBlueprintAmountPerMinute) && gui.CloseDropdown()) { ExportIo(60f); } - if (gui.BuildLink("hour") && gui.CloseDropdown()) { + if (gui.BuildLink(LSs.ExportBlueprintAmountPerHour) && gui.CloseDropdown()) { ExportIo(3600f); } } - if (gui.BuildButton("Remove all zero-building recipes") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableRemoveZeroBuildingRecipes) && gui.CloseDropdown()) { RemoveZeroRecipes(view.model); } - if (gui.BuildRedButton("Clear recipes") && gui.CloseDropdown()) { + if (gui.BuildRedButton(LSs.ProductionTableClearRecipes) && gui.CloseDropdown()) { view.model.RecordUndo().recipes.Clear(); } - if (InputSystem.Instance.control && gui.BuildButton("Add ALL recipes") && gui.CloseDropdown()) { + if (InputSystem.Instance.control && gui.BuildButton(LSs.ProductionTableAddAllRecipes) && gui.CloseDropdown()) { foreach (var recipe in Database.recipes.all) { if (!recipe.IsAccessible()) { continue; @@ -266,14 +267,14 @@ public override void BuildMenu(ImGui gui) { ///
/// The table that will receive the new recipes or technologies, if any are selected private static void BuildRecipeButton(ImGui gui, ProductionTable table) { - if (gui.BuildButton("Add raw recipe").WithTooltip(gui, "Ctrl-click to add a technology instead") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableAddRawRecipe).WithTooltip(gui, LSs.ProductionTableAddTechnologyHint) && gui.CloseDropdown()) { if (InputSystem.Instance.control) { - SelectMultiObjectPanel.Select(Database.technologies.all, "Select technology", + SelectMultiObjectPanel.Select(Database.technologies.all, LSs.SelectTechnology, r => table.AddRecipe(r.With(Quality.Normal), DefaultVariantOrdering), checkMark: r => table.recipes.Any(rr => rr.recipe.target == r), yellowMark: r => table.GetAllRecipes().Any(rr => rr.recipe.target == r)); } else { var prodTable = ProductionLinkSummaryScreen.FindProductionTable(table, out List parents); - SelectMultiObjectPanel.SelectWithQuality(Database.recipes.explorable.AsEnumerable(), "Select raw recipe", + SelectMultiObjectPanel.SelectWithQuality(Database.recipes.explorable.AsEnumerable(), LSs.ProductionTableSelectRawRecipe, r => table.AddRecipe(r, DefaultVariantOrdering), Quality.Normal, checkMark: r => table.recipes.Any(rr => rr.recipe.target == r), yellowMark: r => prodTable?.GetAllRecipes().Any(rr => rr.recipe.target == r) ?? false); } } @@ -305,7 +306,7 @@ private void ExportIo(float multiplier) { } } - private class EntityColumn(ProductionTableView view) : ProductionTableDataColumn(view, "Entity", 8f) { + private class EntityColumn(ProductionTableView view) : ProductionTableDataColumn(view, LSs.ProductionTableHeaderEntity, 8f) { public override void BuildElement(ImGui gui, RecipeRow recipe) { if (recipe.isOverviewMode) { return; @@ -445,7 +446,7 @@ private static void BuildAccumulatorView(ImGui gui, RecipeRow recipe) { private static void ShowAccumulatorDropdown(ImGui gui, RecipeRow recipe, Entity currentAccumulator, Quality accumulatorQuality) => gui.BuildObjectQualitySelectDropDown(Database.allAccumulators, newAccumulator => recipe.RecordUndo().ChangeVariant(currentAccumulator, newAccumulator.target), - new("Select accumulator", ExtraText: x => DataUtils.FormatAmount(x.AccumulatorCapacity(accumulatorQuality), UnitOfMeasure.Megajoule)), + new(LSs.SelectAccumulator, ExtraText: x => DataUtils.FormatAmount(x.AccumulatorCapacity(accumulatorQuality), UnitOfMeasure.Megajoule)), accumulatorQuality, newQuality => recipe.RecordUndo().ChangeVariant(accumulatorQuality, newQuality)); @@ -476,14 +477,14 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { if (!sel.energy.fuels.Contains(recipe.fuel?.target)) { recipe.fuel = recipe.entity.target.energy.fuels.AutoSelect(DataUtils.FavoriteFuel).With(Quality.Normal); } - }, new("Select crafting entity", DataUtils.FavoriteCrafter, ExtraText: x => DataUtils.FormatAmount(x.CraftingSpeed(quality), UnitOfMeasure.Percent))); + }, new(LSs.SelectCraftingEntity, DataUtils.FavoriteCrafter, ExtraText: x => DataUtils.FormatAmount(x.CraftingSpeed(quality), UnitOfMeasure.Percent))); gui.AllocateSpacing(0.5f); if (recipe.fixedBuildings > 0f && (recipe.fixedFuel || recipe.fixedIngredient != null || recipe.fixedProduct != null || !recipe.hierarchyEnabled)) { - ButtonEvent evt = gui.BuildButton("Clear fixed recipe multiplier"); + ButtonEvent evt = gui.BuildButton(LSs.ProductionTableClearFixedMultiplier); if (willResetFixed) { - _ = evt.WithTooltip(gui, "Shortcut: right-click"); + _ = evt.WithTooltip(gui, LSs.ProductionTableShortcutRightClick); } if (evt && gui.CloseDropdown()) { recipe.RecordUndo().fixedBuildings = 0f; @@ -491,23 +492,22 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { } if (recipe.hierarchyEnabled) { - string fixedBuildingsTip = "Tell YAFC how many buildings it must use when solving this page.\n" + - "Use this to ask questions like 'What does it take to handle the output of ten miners?'"; + string fixedBuildingsTip = LSs.ProductionTableFixedBuildingsHint; using (gui.EnterRowWithHelpIcon(fixedBuildingsTip)) { gui.allocator = RectAllocator.RemainingRow; if (recipe.fixedBuildings > 0f && !recipe.fixedFuel && recipe.fixedIngredient == null && recipe.fixedProduct == null) { - ButtonEvent evt = gui.BuildButton("Clear fixed building count"); + ButtonEvent evt = gui.BuildButton(LSs.ProductionTableClearFixedBuildingCount); if (willResetFixed) { - _ = evt.WithTooltip(gui, "Shortcut: right-click"); + _ = evt.WithTooltip(gui, LSs.ProductionTableShortcutRightClick); } if (evt && gui.CloseDropdown()) { recipe.RecordUndo().fixedBuildings = 0f; } } - else if (gui.BuildButton("Set fixed building count") && gui.CloseDropdown()) { + else if (gui.BuildButton(LSs.ProductionTableSetFixedBuildingCount) && gui.CloseDropdown()) { recipe.RecordUndo().fixedBuildings = recipe.buildingCount <= 0f ? 1f : recipe.buildingCount; recipe.fixedFuel = false; recipe.fixedIngredient = null; @@ -517,31 +517,31 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { } } - using (gui.EnterRowWithHelpIcon("Tell YAFC how many of these buildings you have in your factory.\nYAFC will warn you if you need to build more buildings.")) { + using (gui.EnterRowWithHelpIcon(LSs.ProductionTableBuiltBuildingCountHint)) { gui.allocator = RectAllocator.RemainingRow; if (recipe.builtBuildings != null) { - ButtonEvent evt = gui.BuildButton("Clear built building count"); + ButtonEvent evt = gui.BuildButton(LSs.ProductionTableClearBuiltBuildingCount); if (willResetBuilt) { - _ = evt.WithTooltip(gui, "Shortcut: right-click"); + _ = evt.WithTooltip(gui, LSs.ProductionTableShortcutRightClick); } if (evt && gui.CloseDropdown()) { recipe.RecordUndo().builtBuildings = null; } } - else if (gui.BuildButton("Set built building count") && gui.CloseDropdown()) { + else if (gui.BuildButton(LSs.ProductionTableSetBuiltBuildingCount) && gui.CloseDropdown()) { recipe.RecordUndo().builtBuildings = Math.Max(0, Convert.ToInt32(Math.Ceiling(recipe.buildingCount))); recipe.FocusBuiltCountOnNextDraw(); } } if (recipe.entity != null) { - using (gui.EnterRowWithHelpIcon("Generate a blueprint for one of these buildings, with the recipe and internal modules set.")) { + using (gui.EnterRowWithHelpIcon(LSs.ProductionTableGenerateBuildingBlueprintHint)) { gui.allocator = RectAllocator.RemainingRow; - if (gui.BuildButton("Create single building blueprint") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableGenerateBuildingBlueprint) && gui.CloseDropdown()) { BlueprintEntity entity = new BlueprintEntity { index = 1, name = recipe.entity.target.name }; if (!recipe.recipe.Is()) { @@ -568,15 +568,15 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { } if (recipe.recipe.target.crafters.Length > 1) { - BuildFavorites(gui, recipe.entity.target, "Add building to favorites"); + BuildFavorites(gui, recipe.entity.target, LSs.ProductionTableAddBuildingToFavorites); } } }); } public override void BuildMenu(ImGui gui) { - if (gui.BuildButton("Mass set assembler") && gui.CloseDropdown()) { - SelectSingleObjectPanel.Select(Database.allCrafters, "Set assembler for all recipes", set => { + if (gui.BuildButton(LSs.ProductionTableMassSetAssembler) && gui.CloseDropdown()) { + SelectSingleObjectPanel.Select(Database.allCrafters, LSs.ProductionTableSelectMassAssembler, set => { DataUtils.FavoriteCrafter.AddToFavorite(set, 10); foreach (var recipe in view.GetRecipesRecursive()) { @@ -592,14 +592,14 @@ public override void BuildMenu(ImGui gui) { }, DataUtils.FavoriteCrafter); } - if (gui.BuildQualityList(null, out Quality? quality, "Mass set quality") && gui.CloseDropdown()) { + if (gui.BuildQualityList(null, out Quality? quality, LSs.ProductionTableMassSetQuality) && gui.CloseDropdown()) { foreach (RecipeRow recipe in view.GetRecipesRecursive()) { recipe.RecordUndo().entity = recipe.entity?.With(quality); } } - if (gui.BuildButton("Mass set fuel") && gui.CloseDropdown()) { - SelectSingleObjectPanel.SelectWithQuality(Database.goods.all.Where(x => x.fuelValue > 0), "Set fuel for all recipes", set => { + if (gui.BuildButton(LSs.ProductionTableMassSetFuel) && gui.CloseDropdown()) { + SelectSingleObjectPanel.SelectWithQuality(Database.goods.all.Where(x => x.fuelValue > 0), LSs.ProductionTableSelectMassFuel, set => { DataUtils.FavoriteFuel.AddToFavorite(set.target, 10); foreach (var recipe in view.GetRecipesRecursive()) { @@ -610,13 +610,13 @@ public override void BuildMenu(ImGui gui) { }, null, DataUtils.FavoriteFuel); } - if (gui.BuildButton("Shopping list") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ShoppingList) && gui.CloseDropdown()) { view.BuildShoppingList(null); } } } - private class IngredientsColumn(ProductionTableView view) : ProductionTableDataColumn(view, "Ingredients", 32f, 16f, 100f, hasMenu: false, nameof(Preferences.ingredientsColumWidth)) { + private class IngredientsColumn(ProductionTableView view) : ProductionTableDataColumn(view, LSs.ProductionTableHeaderIngredients, 32f, 16f, 100f, hasMenu: false, nameof(Preferences.ingredientsColumWidth)) { public override void BuildElement(ImGui gui, RecipeRow recipe) { var grid = gui.EnterInlineGrid(3f, 1f); @@ -638,7 +638,7 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { } } - private class ProductsColumn(ProductionTableView view) : ProductionTableDataColumn(view, "Products", 12f, 10f, 70f, hasMenu: false, nameof(Preferences.productsColumWidth)) { + private class ProductsColumn(ProductionTableView view) : ProductionTableDataColumn(view, LSs.ProductionTableHeaderProducts, 12f, 10f, 70f, hasMenu: false, nameof(Preferences.productsColumWidth)) { public override void BuildElement(ImGui gui, RecipeRow recipe) { var grid = gui.EnterInlineGrid(3f, 1f); if (recipe.isOverviewMode) { @@ -650,7 +650,7 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { if (recipe.recipe.target is Recipe { preserveProducts: true }) { view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, new() { HintLocations = HintLocations.OnConsumingRecipes, - ExtraSpoilInformation = gui => gui.BuildText("This recipe output does not start spoiling until removed from the machine.", TextBlockDisplayStyle.WrappedText) + ExtraSpoilInformation = gui => gui.BuildText(LSs.ProductionTableOutputPreservedInMachine, TextBlockDisplayStyle.WrappedText) }); } else if (percentSpoiled == null) { @@ -658,12 +658,12 @@ public override void BuildElement(ImGui gui, RecipeRow recipe) { } else if (percentSpoiled == 0) { view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, - new() { HintLocations = HintLocations.OnConsumingRecipes, ExtraSpoilInformation = gui => gui.BuildText("This recipe output is always fresh.") }); + new() { HintLocations = HintLocations.OnConsumingRecipes, ExtraSpoilInformation = gui => gui.BuildText(LSs.ProductionTableOutputAlwaysFresh) }); } else { view.BuildGoodsIcon(gui, goods, link, amount, ProductDropdownType.Product, recipe, recipe.linkRoot, new() { HintLocations = HintLocations.OnConsumingRecipes, - ExtraSpoilInformation = gui => gui.BuildText($"This recipe output is {DataUtils.FormatAmount(percentSpoiled.Value, UnitOfMeasure.Percent)} spoiled.") + ExtraSpoilInformation = gui => gui.BuildText(LSs.ProductionTableOutputFixedSpoilage.L(DataUtils.FormatAmount(percentSpoiled.Value, UnitOfMeasure.Percent))) }); } } @@ -681,7 +681,7 @@ private class ModulesColumn : ProductionTableDataColumn { private readonly VirtualScrollList moduleTemplateList; private RecipeRow editingRecipeModules = null!; // null-forgiving: This is set as soon as we open a module dropdown. - public ModulesColumn(ProductionTableView view) : base(view, "Modules", 10f, 7f, 16f, widthStorage: nameof(Preferences.modulesColumnWidth)) + public ModulesColumn(ProductionTableView view) : base(view, LSs.ProductionTableHeaderModules, 10f, 7f, 16f, widthStorage: nameof(Preferences.modulesColumnWidth)) => moduleTemplateList = new VirtualScrollList(15f, new Vector2(20f, 2.5f), ModuleTemplateDrawer, collapsible: true); private void ModuleTemplateDrawer(ImGui gui, ProjectModuleTemplate element, int index) { @@ -740,7 +740,7 @@ void drawItem(ImGui gui, IObjectWithQuality? item, int count) { private void ShowModuleTemplateTooltip(ImGui gui, ModuleTemplate template) => gui.ShowTooltip(imGui => { if (!template.IsCompatibleWith(editingRecipeModules)) { - imGui.BuildText("This module template seems incompatible with the recipe or the building", TextBlockDisplayStyle.WrappedText); + imGui.BuildText(LSs.ProductionTableModuleTemplateIncompatible, TextBlockDisplayStyle.WrappedText); } using var grid = imGui.EnterInlineGrid(3f, 1f); @@ -769,7 +769,7 @@ private void ShowModuleDropDown(ImGui gui, RecipeRow recipe) { Quality quality = Quality.Normal; gui.ShowDropDown(dropGui => { - if (recipe.modules != null && dropGui.BuildButton("Use default modules").WithTooltip(dropGui, "Shortcut: right-click") && dropGui.CloseDropdown()) { + if (recipe.modules != null && dropGui.BuildButton(LSs.ProductionTableUseDefaultModules).WithTooltip(dropGui, LSs.ProductionTableShortcutRightClick) && dropGui.CloseDropdown()) { recipe.RemoveFixedModules(); } @@ -785,18 +785,18 @@ private void ShowModuleDropDown(ImGui gui, RecipeRow recipe) { else { _ = dropGui.BuildQualityList(quality, out quality); } - dropGui.BuildInlineObjectListAndButton(modules, m => recipe.SetFixedModule(m.With(quality)), new("Select fixed module", DataUtils.FavoriteModule)); + dropGui.BuildInlineObjectListAndButton(modules, m => recipe.SetFixedModule(m.With(quality)), new(LSs.ProductionTableSelectModules, DataUtils.FavoriteModule)); } if (moduleTemplateList.data.Count > 0) { - dropGui.BuildText("Use module template:", Font.subheader); + dropGui.BuildText(LSs.ProductionTableUseModuleTemplate, Font.subheader); moduleTemplateList.Build(dropGui); } - if (dropGui.BuildButton("Configure module templates") && dropGui.CloseDropdown()) { + if (dropGui.BuildButton(LSs.ProductionTableConfigureModuleTemplates) && dropGui.CloseDropdown()) { ModuleTemplateConfiguration.Show(); } - if (dropGui.BuildButton("Customize modules") && dropGui.CloseDropdown()) { + if (dropGui.BuildButton(LSs.ProductionTableCustomizeModules) && dropGui.CloseDropdown()) { ModuleCustomizationScreen.Show(recipe); } }); @@ -805,15 +805,15 @@ private void ShowModuleDropDown(ImGui gui, RecipeRow recipe) { public override void BuildMenu(ImGui gui) { var model = view.model; - gui.BuildText("Auto modules", Font.subheader); + gui.BuildText(LSs.ProductionTableAutoModules, Font.subheader); ModuleFillerParametersScreen.BuildSimple(gui, model.modules!); // null-forgiving: owner is a ProjectPage, so modules is not null. - if (gui.BuildButton("Module settings") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableModuleSettings) && gui.CloseDropdown()) { ModuleFillerParametersScreen.Show(model.modules!); } } } - public static void BuildFavorites(ImGui imgui, FactorioObject? obj, string prompt) { + public static void BuildFavorites(ImGui imgui, FactorioObject? obj, LocalizableString0 prompt) { if (obj == null) { return; } @@ -821,7 +821,7 @@ public static void BuildFavorites(ImGui imgui, FactorioObject? obj, string promp bool isFavorite = Project.current.preferences.favorites.Contains(obj); using (imgui.EnterRow(0.5f, RectAllocator.LeftRow)) { imgui.BuildIcon(isFavorite ? Icon.StarFull : Icon.StarEmpty); - imgui.RemainingRow().BuildText(isFavorite ? "Favorite" : prompt); + imgui.RemainingRow().BuildText(isFavorite ? LSs.Favorite : prompt); } if (imgui.OnClick(imgui.lastRect)) { Project.current.preferences.ToggleFavorite(obj); @@ -909,7 +909,7 @@ async void addRecipe(RecipeOrTechnology rec) { } } - if (!curLevelRecipes.Contains(qualityRecipe) || (await MessageBox.Show("Recipe already exists", $"Add a second copy of {rec.locName}?", "Add a copy", "Cancel")).choice) { + if (!curLevelRecipes.Contains(qualityRecipe) || (await MessageBox.Show(LSs.ProductionTableAlertRecipeExists, LSs.ProductionTableQueryAddCopy.L(rec.locName), LSs.ProductionTableAddCopy, LSs.Cancel)).choice) { context.AddRecipe(qualityRecipe, DefaultVariantOrdering, selectedFuel, spentFuel); } } @@ -942,21 +942,21 @@ void dropDownContent(ImGui gui) { EntityEnergy? energy = recipe.entity.target.energy; if (energy == null || energy.fuels.Length == 0) { - gui.BuildText("This entity has no known fuels"); + gui.BuildText(LSs.ProductionTableAlertNoKnownFuels); } else if (energy.fuels.Length > 1 || energy.fuels[0] != recipe.fuel?.target) { Func fuelDisplayFunc = energy.type == EntityEnergyType.FluidHeat ? g => DataUtils.FormatAmount(g.fluid?.heatValue ?? 0, UnitOfMeasure.Megajoule) : g => DataUtils.FormatAmount(g.fuelValue, UnitOfMeasure.Megajoule); - BuildFavorites(gui, recipe.fuel!.target, "Add fuel to favorites"); + BuildFavorites(gui, recipe.fuel!.target, LSs.ProductionTableAddFuelToFavorites); gui.BuildInlineObjectListAndButton(energy.fuels, fuel => recipe.RecordUndo().fuel = fuel.With(Quality.Normal), - new("Select fuel", DataUtils.FavoriteFuel, ExtraText: fuelDisplayFunc)); + new(LSs.ProductionTableSelectFuel, DataUtils.FavoriteFuel, ExtraText: fuelDisplayFunc)); } } if (variants != null) { - gui.BuildText("Accepted fluid variants:"); + gui.BuildText(LSs.ProductionTableAcceptedFluids); using (var grid = gui.EnterInlineGrid(3f)) { foreach (var variant in variants) { grid.Next(); @@ -990,13 +990,13 @@ void dropDownContent(ImGui gui) { int numberOfShownRecipes = 0; if (goods == Database.science) { - if (gui.BuildButton("Add technology") && gui.CloseDropdown()) { - SelectMultiObjectPanel.Select(Database.technologies.all, "Select technology", + if (gui.BuildButton(LSs.ProductionTableAddTechnology) && gui.CloseDropdown()) { + SelectMultiObjectPanel.Select(Database.technologies.all, LSs.SelectTechnology, r => context.AddRecipe(r.With(Quality.Normal), DefaultVariantOrdering), checkMark: r => context.recipes.Any(rr => rr.recipe.target == r)); } } else if (type <= ProductDropdownType.Ingredient && allProduction.Length > 0) { - gui.BuildInlineObjectListAndButton(allProduction, addRecipe, new("Add production recipe", comparer, 6, true, recipeExists, recipeExistsAnywhere)); + gui.BuildInlineObjectListAndButton(allProduction, addRecipe, new(LSs.ProductionTableAddProductionRecipe, comparer, 6, true, recipeExists, recipeExistsAnywhere)); numberOfShownRecipes += allProduction.Length; if (iLink == null) { @@ -1008,7 +1008,7 @@ void dropDownContent(ImGui gui) { CreateNewProductionTable(goods, amount); } else if (evt == ButtonEvent.MouseOver) { - gui.ShowTooltip(iconRect, "Create new production table for " + goods.target.locName); + gui.ShowTooltip(iconRect, LSs.ProductionTableCreateNewTable.L(goods.target.locName)); } } } @@ -1017,7 +1017,7 @@ void dropDownContent(ImGui gui) { gui.BuildInlineObjectListAndButton( spentFuelRecipes, (x) => { spentFuel = goods; addRecipe(x); }, - new("Produce it as a spent fuel", + new(LSs.ProductionTableProduceAsSpentFuel, DataUtils.AlreadySortedRecipe, 3, true, @@ -1030,7 +1030,7 @@ void dropDownContent(ImGui gui) { gui.BuildInlineObjectListAndButton( goods.target.usages, addRecipe, - new("Add consumption recipe", + new(LSs.ProductionTableAddConsumptionRecipe, DataUtils.DefaultRecipeOrdering, 6, true, @@ -1043,7 +1043,7 @@ void dropDownContent(ImGui gui) { gui.BuildInlineObjectListAndButton( fuelUseList, (x) => { selectedFuel = goods; addRecipe(x); }, - new("Add fuel usage", + new(LSs.ProductionTableAddFuelUsage, DataUtils.AlreadySortedRecipe, 6, true, @@ -1053,74 +1053,71 @@ void dropDownContent(ImGui gui) { } if (type >= ProductDropdownType.Product && Database.allSciencePacks.Contains(goods.target) - && gui.BuildButton("Add consumption technology") && gui.CloseDropdown()) { + && gui.BuildButton(LSs.ProductionTableAddConsumptionTechnology) && gui.CloseDropdown()) { // Select from the technologies that consume this science pack. SelectMultiObjectPanel.Select(Database.technologies.all.Where(t => t.ingredients.Select(i => i.goods).Contains(goods.target)), - "Add technology", addRecipe, checkMark: recipeExists, yellowMark: recipeExistsAnywhere); + LSs.ProductionTableAddTechnology, addRecipe, checkMark: recipeExists, yellowMark: recipeExistsAnywhere); } if (type >= ProductDropdownType.Product && allProduction.Length > 0) { - gui.BuildInlineObjectListAndButton(allProduction, addRecipe, new("Add production recipe", comparer, 1, true, recipeExists, recipeExistsAnywhere)); + gui.BuildInlineObjectListAndButton(allProduction, addRecipe, new(LSs.ProductionTableAddProductionRecipe, comparer, 1, true, recipeExists, recipeExistsAnywhere)); numberOfShownRecipes += allProduction.Length; } if (numberOfShownRecipes > 1) { - gui.BuildText("Hint: ctrl+click to add multiple", TextBlockDisplayStyle.HintText); + gui.BuildText(LSs.ProductionTableAddMultipleHint, TextBlockDisplayStyle.HintText); } #endregion #region Link management ProductionLink? link = iLink as ProductionLink; - if (link != null && gui.BuildCheckBox("Allow overproduction", link.algorithm == LinkAlgorithm.AllowOverProduction, out bool newValue)) { + if (link != null && gui.BuildCheckBox(LSs.ProductionTableAllowOverproduction, link.algorithm == LinkAlgorithm.AllowOverProduction, out bool newValue)) { link.RecordUndo().algorithm = newValue ? LinkAlgorithm.AllowOverProduction : LinkAlgorithm.Match; } - if (iLink != null && gui.BuildButton("View link summary") && gui.CloseDropdown()) { + if (iLink != null && gui.BuildButton(LSs.ProductionTableViewLinkSummary) && gui.CloseDropdown()) { ProductionLinkSummaryScreen.Show(iLink.DisplayLink); } if (link != null && link.owner == context) { if (link.amount != 0) { - gui.BuildText(goods.target.locName + " is a desired product and cannot be unlinked.", TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ProductionTableCannotUnlink.L(goods.target.locName), TextBlockDisplayStyle.WrappedText); } else { - string goodProdLinkedMessage = goods.target.locName + " production is currently linked. This means that YAFC will try to match production with consumption."; + string goodProdLinkedMessage = LSs.ProductionTableCurrentlyLinked.L(goods.target.locName); gui.BuildText(goodProdLinkedMessage, TextBlockDisplayStyle.WrappedText); } if (type is ProductDropdownType.DesiredIngredient or ProductDropdownType.DesiredProduct) { - if (gui.BuildButton("Remove desired product") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableRemoveDesiredProduct) && gui.CloseDropdown()) { link.RecordUndo().amount = 0; } - if (gui.BuildButton("Remove and unlink").WithTooltip(gui, "Shortcut: right-click") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableRemoveAndUnlinkDesiredProduct).WithTooltip(gui, LSs.ProductionTableShortcutRightClick) && gui.CloseDropdown()) { DestroyLink(link); } } - else if (link.amount == 0 && gui.BuildButton("Unlink").WithTooltip(gui, "Shortcut: right-click") && gui.CloseDropdown()) { + else if (link.amount == 0 && gui.BuildButton(LSs.ProductionTableUnlink).WithTooltip(gui, LSs.ProductionTableShortcutRightClick) && gui.CloseDropdown()) { DestroyLink(link); } } else if (goods != null) { if (link != null) { - string goodsNestLinkMessage = goods.target.locName + " production is currently linked, but the link is outside this nested table. " + - "Nested tables can have its own separate set of links"; + string goodsNestLinkMessage = LSs.ProductionTableLinkedInParent.L(goods.target.locName); gui.BuildText(goodsNestLinkMessage, TextBlockDisplayStyle.WrappedText); } else if (iLink != null) { - string implicitLink = goods.target.locName + $" ({goods.quality.locName}) production is implicitly linked. This means that YAFC will use it, " + - $"along with all other available qualities, to produce {Database.science.target.locName}.\n" + - $"You may add a regular link to replace this implicit link."; + string implicitLink = LSs.ProductionTableImplicitlyLinked.L(goods.target.locName, goods.quality.locName, Database.science.target.locName); gui.BuildText(implicitLink, TextBlockDisplayStyle.WrappedText); - if (gui.BuildButton("Create link").WithTooltip(gui, "Shortcut: right-click") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableCreateLink).WithTooltip(gui, LSs.ProductionTableShortcutRightClick) && gui.CloseDropdown()) { CreateLink(context, goods); } } else if (goods.target.isLinkable) { - string notLinkedMessage = goods.target.locName + " production is currently NOT linked. This means that YAFC will make no attempt to match production with consumption."; + string notLinkedMessage = LSs.ProductionTableNotLinked.L(goods.target.locName); gui.BuildText(notLinkedMessage, TextBlockDisplayStyle.WrappedText); - if (gui.BuildButton("Create link").WithTooltip(gui, "Shortcut: right-click") && gui.CloseDropdown()) { + if (gui.BuildButton(LSs.ProductionTableCreateLink).WithTooltip(gui, LSs.ProductionTableShortcutRightClick) && gui.CloseDropdown()) { CreateLink(context, goods); } } @@ -1134,9 +1131,9 @@ void dropDownContent(ImGui gui) { || (type == ProductDropdownType.Ingredient && recipe.fixedIngredient != goods) || (type == ProductDropdownType.Product && recipe.fixedProduct != goods)) { string? prompt = type switch { - ProductDropdownType.Fuel => "Set fixed fuel consumption", - ProductDropdownType.Ingredient => "Set fixed ingredient consumption", - ProductDropdownType.Product => "Set fixed production amount", + ProductDropdownType.Fuel => LSs.ProductionTableSetFixedFuel, + ProductDropdownType.Ingredient => LSs.ProductionTableSetFixedIngredient, + ProductDropdownType.Product => LSs.ProductionTableSetFixedProduct, _ => null }; if (prompt != null) { @@ -1145,7 +1142,7 @@ void dropDownContent(ImGui gui) { evt = gui.BuildButton(prompt); } else { - using (gui.EnterRowWithHelpIcon("This will replace the other fixed amount in this row.")) { + using (gui.EnterRowWithHelpIcon(LSs.ProductionTableSetFixedWillReplace)) { gui.allocator = RectAllocator.RemainingRow; evt = gui.BuildButton(prompt); } @@ -1182,9 +1179,9 @@ void dropDownContent(ImGui gui) { || (type == ProductDropdownType.Ingredient && recipe.fixedIngredient == goods) || (type == ProductDropdownType.Product && recipe.fixedProduct == goods))) { string? prompt = type switch { - ProductDropdownType.Fuel => "Clear fixed fuel consumption", - ProductDropdownType.Ingredient => "Clear fixed ingredient consumption", - ProductDropdownType.Product => "Clear fixed production amount", + ProductDropdownType.Fuel => LSs.ProductionTableClearFixedFuel, + ProductDropdownType.Ingredient => LSs.ProductionTableClearFixedIngredient, + ProductDropdownType.Product => LSs.ProductionTableClearFixedProduct, _ => null }; if (prompt != null && gui.BuildButton(prompt) && gui.CloseDropdown()) { @@ -1428,7 +1425,7 @@ private static void BuildBeltInserterInfo(ImGui gui, Item item, float amount, fl gui.BuildText(DataUtils.FormatAmount(beltCount, UnitOfMeasure.None)); if (buildingsPerHalfBelt > 0f) { - gui.BuildText("(Buildings per half belt: " + DataUtils.FormatAmount(buildingsPerHalfBelt, UnitOfMeasure.None) + ")"); + gui.BuildText(LSs.ProductionTableBuildingsPerHalfBelt.L(DataUtils.FormatAmount(buildingsPerHalfBelt, UnitOfMeasure.None))); } } @@ -1439,7 +1436,8 @@ private static void BuildBeltInserterInfo(ImGui gui, Item item, float amount, fl string text = DataUtils.FormatAmount(inserterBase, UnitOfMeasure.None); if (buildingCount > 1) { - text += " (" + DataUtils.FormatAmount(inserterBase / buildingCount, UnitOfMeasure.None) + "/building)"; + text = LSs.ProductionTableInsertersPerBuilding.L(DataUtils.FormatAmount(inserterBase, UnitOfMeasure.None), + DataUtils.FormatAmount(inserterBase / buildingCount, UnitOfMeasure.None)); } gui.BuildText(text); @@ -1450,10 +1448,11 @@ private static void BuildBeltInserterInfo(ImGui gui, Item item, float amount, fl click |= gui.BuildFactorioObjectButton(belt, ButtonDisplayStyle.Default) == Click.Left; gui.AllocateSpacing(-1.5f); click |= gui.BuildFactorioObjectButton(inserter, ButtonDisplayStyle.Default) == Click.Left; - text = "~" + DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None); + text = LSs.ProductionTableApproximateNumber.L(DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None)); if (buildingCount > 1) { - text += " (" + DataUtils.FormatAmount(inserterToBelt / buildingCount, UnitOfMeasure.None) + "/b)"; + text = LSs.ProductionTableApproximateInsertersPerBuilding.L(DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None), + DataUtils.FormatAmount(inserterToBelt / buildingCount, UnitOfMeasure.None)); } gui.BuildText(text); @@ -1532,29 +1531,24 @@ protected override void BuildPageTooltip(ImGui gui, ProductionTable contents) { } } - private static readonly Dictionary WarningsMeaning = new Dictionary + private static readonly Dictionary WarningsMeaning = new() { - {WarningFlags.DeadlockCandidate, "Contains recursive links that cannot be matched. No solution exists."}, - {WarningFlags.OverproductionRequired, "This model cannot be solved exactly, it requires some overproduction. You can allow overproduction for any link. " + - "This recipe contains one of the possible candidates."}, - {WarningFlags.EntityNotSpecified, "Crafter not specified. Solution is inaccurate." }, - {WarningFlags.FuelNotSpecified, "Fuel not specified. Solution is inaccurate." }, - {WarningFlags.FuelWithTemperatureNotLinked, "This recipe uses fuel with temperature. Should link with producing entity to determine temperature."}, - {WarningFlags.FuelTemperatureExceedsMaximum, "Fluid temperature is higher than generator maximum. Some energy is wasted."}, - {WarningFlags.FuelDoesNotProvideEnergy, "This fuel cannot provide any energy to this building. The building won't work."}, - {WarningFlags.FuelUsageInputLimited, "This building has max fuel consumption. The rate at which it works is limited by it."}, - {WarningFlags.TemperatureForIngredientNotMatch, "This recipe does care about ingredient temperature, and the temperature range does not match"}, - {WarningFlags.ReactorsNeighborsFromPrefs, "Assumes reactor formation from preferences. (Click to open the preferences)"}, - {WarningFlags.AssumesNauvisSolarRatio, "Energy production values assumes Nauvis solar ration (70% power output). Don't forget accumulators."}, - {WarningFlags.ExceedsBuiltCount, "This recipe requires more buildings than are currently built."}, - {WarningFlags.AsteroidCollectionNotModelled, "The speed of asteroid collectors depends heavily on location and travel speed. " + - "It also depends on the distance between adjacent collectors. These dependencies are not modeled. Expect widely varied performance."}, - {WarningFlags.AssumesFulgoraAndModel, "Energy production values assume Fulgoran storms and attractors in a square grid.\n" + - "The accumulator estimate tries to store 10% of the energy captured by the attractors."}, - {WarningFlags.UselessQuality, "The quality bonus on this recipe has no effect. " + - "Make sure the recipe produces items and that all milestones for the next quality are unlocked. (Click to open the milestone window)"}, - {WarningFlags.ExcessProductivity, "This building has a larger productivity bonus (from base effect, research, and/or modules) than allowed by the recipe. " + - "Please make sure you entered productivity research levels, not percent bonuses. (Click to open the preferences)"}, + {WarningFlags.DeadlockCandidate, LSs.WarningDescriptionDeadlockCandidate}, + {WarningFlags.OverproductionRequired, LSs.WarningDescriptionOverproductionRequired}, + {WarningFlags.EntityNotSpecified, LSs.WarningDescriptionEntityNotSpecified}, + {WarningFlags.FuelNotSpecified, LSs.WarningDescriptionFuelNotSpecified}, + {WarningFlags.FuelWithTemperatureNotLinked, LSs.WarningDescriptionFluidWithTemperature}, + {WarningFlags.FuelTemperatureExceedsMaximum, LSs.WarningDescriptionFluidTooHot}, + {WarningFlags.FuelDoesNotProvideEnergy, LSs.WarningDescriptionFuelDoesNotProvideEnergy}, + {WarningFlags.FuelUsageInputLimited, LSs.WarningDescriptionHasMaxFuelConsumption}, + {WarningFlags.TemperatureForIngredientNotMatch, LSs.WarningDescriptionIngredientTemperatureRange}, + {WarningFlags.ReactorsNeighborsFromPrefs, LSs.WarningDescriptionAssumesReactorFormation}, + {WarningFlags.AssumesNauvisSolarRatio, LSs.WarningDescriptionAssumesNauvisSolar}, + {WarningFlags.ExceedsBuiltCount, LSs.WarningDescriptionNeedsMoreBuildings}, + {WarningFlags.AsteroidCollectionNotModelled, LSs.WarningDescriptionAsteroidCollectors}, + {WarningFlags.AssumesFulgoraAndModel, LSs.WarningDescriptionAssumesFulgoranLightning}, + {WarningFlags.UselessQuality, LSs.WarningDescriptionUselessQuality}, + {WarningFlags.ExcessProductivity, LSs.WarningDescriptionExcessProductivityBonus}, }; private static readonly (Icon icon, SchemeColor color)[] tagIcons = [ @@ -1581,7 +1575,7 @@ protected override void BuildContent(ImGui gui) { } private static void AddDesiredProductAtLevel(ProductionTable table) => SelectMultiObjectPanel.SelectWithQuality( - Database.goods.all.Except(table.linkMap.Where(p => p.Value.amount != 0).Select(p => p.Key.target)).Where(g => g.isLinkable), "Add desired product", product => { + Database.goods.all.Except(table.linkMap.Where(p => p.Value.amount != 0).Select(p => p.Key.target)).Where(g => g.isLinkable), LSs.ProductionTableAddDesiredProduct, product => { if (table.linkMap.TryGetValue(product, out var existing) && existing is ProductionLink link) { if (link.amount != 0) { return; @@ -1604,7 +1598,7 @@ private void BuildSummary(ImGui gui, ProductionTable table) { gui.spacing = 1f; Padding pad = new Padding(1f, 0.2f); using (gui.EnterGroup(pad)) { - gui.BuildText("Desired products and amounts (Use negative for input goal):"); + gui.BuildText(LSs.ProductionTableDesiredProducts); using var grid = gui.EnterInlineGrid(3f, 1f, elementsPerRow); foreach (var link in table.links.ToList()) { if (link.amount != 0f) { @@ -1625,7 +1619,7 @@ private void BuildSummary(ImGui gui, ProductionTable table) { if (table.flow.Length > 0 && table.flow[0].amount < 0) { using (gui.EnterGroup(pad)) { - gui.BuildText(isRoot ? "Summary ingredients:" : "Import ingredients:"); + gui.BuildText(isRoot ? LSs.ProductionTableSummaryIngredients : LSs.ProductionTableImportIngredients); var grid = gui.EnterInlineGrid(3f, 1f, elementsPerRow); BuildTableIngredients(gui, table, table, ref grid); grid.Dispose(); @@ -1641,7 +1635,7 @@ private void BuildSummary(ImGui gui, ProductionTable table) { ImGuiUtils.InlineGridBuilder grid = default; void initializeGrid(ImGui gui) { context = gui.EnterGroup(pad); - gui.BuildText(isRoot ? "Extra products:" : "Export products:"); + gui.BuildText(isRoot ? LSs.ProductionTableExtraProducts : LSs.ProductionTableExportProducts); grid = gui.EnterInlineGrid(3f, 1f, elementsPerRow); } diff --git a/Yafc/Workspace/SummaryView.cs b/Yafc/Workspace/SummaryView.cs index 30ac7c6d..c204797c 100644 --- a/Yafc/Workspace/SummaryView.cs +++ b/Yafc/Workspace/SummaryView.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using System.Threading.Tasks; +using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -26,7 +27,7 @@ private class SummaryScrollArea(GuiBuilder builder) : ScrollArea(DefaultHeight, } private class SummaryTabColumn : TextDataColumn { - public SummaryTabColumn() : base("Tab", firstColumnWidth) { + public SummaryTabColumn() : base(LSs.Page, firstColumnWidth) { } public override void BuildElement(ImGui gui, ProjectPage page) { @@ -51,7 +52,7 @@ public override void BuildElement(ImGui gui, ProjectPage page) { } } - private sealed class SummaryDataColumn(SummaryView view) : TextDataColumn("Linked", float.MaxValue) { + private sealed class SummaryDataColumn(SummaryView view) : TextDataColumn(LSs.SummaryColumnLinked, float.MaxValue) { public override void BuildElement(ImGui gui, ProjectPage page) { if (page?.contentType != typeof(ProductionTable)) { return; @@ -232,7 +233,7 @@ protected override void BuildHeader(ImGui gui) { base.BuildHeader(gui); gui.allocator = RectAllocator.Center; - gui.BuildText("Production Sheet Summary", new TextBlockDisplayStyle(Font.header, Alignment: RectAlignment.Middle)); + gui.BuildText(LSs.SummaryHeader, new TextBlockDisplayStyle(Font.header, Alignment: RectAlignment.Middle)); gui.allocator = RectAllocator.LeftAlign; } @@ -259,13 +260,13 @@ protected override async void BuildContent(ImGui gui) { using (gui.EnterRow()) { _ = gui.AllocateRect(0, 2); // Increase row height to 2, for vertical centering. - if (gui.BuildCheckBox("Only show issues", model?.showOnlyIssues ?? false, out bool newValue)) { + if (gui.BuildCheckBox(LSs.SummaryOnlyShowIssues, model?.showOnlyIssues ?? false, out bool newValue)) { model!.showOnlyIssues = newValue; // null-forgiving: when model is null, the page is no longer being displayed, so no clicks can happen. Recalculate(); } - using (gui.EnterRowWithHelpIcon("Attempt to match production and consumption of all linked products on the displayed pages.\n\nYou will often have to click this button multiple times to fully balance production.", false)) { - if (gui.BuildButton("Auto balance")) { + using (gui.EnterRowWithHelpIcon(LSs.SummaryAutoBalanceHint, false)) { + if (gui.BuildButton(LSs.SummaryAutoBalance)) { await AutoBalance(); } } From 15da308e3c69d6f5922b4278b7dd2ac7998c304f Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:12:12 -0400 Subject: [PATCH 33/54] feat(i18n): Merge similar strings, add plurals, and proofread. --- Yafc.Model/Analysis/DependencyNode.cs | 6 + Yafc.UI/Core/ExceptionScreen.cs | 2 +- Yafc.UI/ImGui/ImGuiBuilding.cs | 4 + Yafc/Data/locale/en/yafc.cfg | 277 +++++++++--------- Yafc/Widgets/ObjectTooltip.cs | 35 ++- Yafc/Windows/DependencyExplorer.cs | 32 +- Yafc/Windows/MainScreen.cs | 10 +- Yafc/Windows/MilestonesEditor.cs | 3 +- Yafc/Windows/PreferencesScreen.cs | 7 +- Yafc/Windows/WelcomeScreen.cs | 13 +- .../ModuleFillerParametersScreen.cs | 4 +- .../ProductionTable/ProductionTableView.cs | 33 +-- changelog.txt | 1 + 13 files changed, 222 insertions(+), 205 deletions(-) diff --git a/Yafc.Model/Analysis/DependencyNode.cs b/Yafc.Model/Analysis/DependencyNode.cs index 2719cde3..5c6b3fab 100644 --- a/Yafc.Model/Analysis/DependencyNode.cs +++ b/Yafc.Model/Analysis/DependencyNode.cs @@ -69,6 +69,12 @@ private DependencyNode() { } // All derived classes should be nested classes ///
internal abstract IEnumerable Flatten(); + /// + /// Gets the number of entries in this dependency tree. + /// + /// + public int Count() => Flatten().Count(); + /// /// Determines whether the object that owns this dependency tree is accessible, based on the accessibility /// returns for dependent objects, and the types of this node and its children. diff --git a/Yafc.UI/Core/ExceptionScreen.cs b/Yafc.UI/Core/ExceptionScreen.cs index d6a0ed13..59b6d871 100644 --- a/Yafc.UI/Core/ExceptionScreen.cs +++ b/Yafc.UI/Core/ExceptionScreen.cs @@ -46,7 +46,7 @@ protected override void BuildContents(ImGui gui) { Close(); } - if (gui.BuildButton(LSs.IgnoreFutureErrors, SchemeColor.Grey)) { + if (gui.BuildButton(LSs.ExceptionIgnoreFutureErrors, SchemeColor.Grey)) { ignoreAll = true; Close(); } diff --git a/Yafc.UI/ImGui/ImGuiBuilding.cs b/Yafc.UI/ImGui/ImGuiBuilding.cs index 0db59678..c8e7c5f1 100644 --- a/Yafc.UI/ImGui/ImGuiBuilding.cs +++ b/Yafc.UI/ImGui/ImGuiBuilding.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Numerics; using SDL2; @@ -139,6 +140,9 @@ public Rect AllocateTextRect(out TextCache? cache, string? text, TextBlockDispla rect = AllocateRect(0f, topOffset + (fontSize.lineSize / pixelsPerUnit)); } else { + if (Debugger.IsAttached && text.Contains("Key not found: ")) { + Debugger.Break(); // drawing improperly internationalized text + } Vector2 textSize = GetTextDimensions(out cache, text, displayStyle.Font, displayStyle.WrapText, maxWidth); rect = AllocateRect(textSize.X, topOffset + (textSize.Y), displayStyle.Alignment); } diff --git a/Yafc/Data/locale/en/yafc.cfg b/Yafc/Data/locale/en/yafc.cfg index 71aeadcc..a60d5cbc 100644 --- a/Yafc/Data/locale/en/yafc.cfg +++ b/Yafc/Data/locale/en/yafc.cfg @@ -9,6 +9,7 @@ could-not-read-mod-list=Could not read mod list from __1__ mod-not-found-try-in-factorio=Mod not found: __1__. Try loading this pack in Factorio first. progress-creating-lua-context=Creating Lua context circular-mod-dependencies=Mods dependencies are circular. Unable to load mods: __1__ +; In most languages, this should have a trailing space: list-separator=, progress-completed=Completed! @@ -65,10 +66,11 @@ entity-crafting-speed=Crafting speed: __1__ entity-crafting-productivity=Crafting productivity: __1__ entity-energy-consumption=Energy consumption: __1__ entity-module-slots=Module slots: __1__ -allowed-module-effects-untranslated-list=Only allowed effects: __1__ -lab-allowed-inputs=Allowed inputs: +; __1__ is the list, and __2__ is the length of the list +allowed-module-effects-untranslated-list=Only allowed effect__plural_for_parameter__2__{1=|rest=s}__: __1__ +lab-allowed-inputs=Allowed input__plural_for_parameter__1__{1=|rest=s}__: perishable=Perishable -tooltip-add-drain-energy=__1__ + __2__ +tooltip-active-plus-drain-power=__1__ + __2__ entity-absorbs-pollution=This building absorbs __1__ entity-has-high-pollution=This building contributes to global warning! belt-throughput=Belt throughput (Items): __1__ @@ -77,10 +79,12 @@ beacon-efficiency=Beacon efficiency: __1__ accumulator-capacity=Accumulator charge: __1__ lightning-attractor-extra-info=Power production (average usable): __1__\n Build in a __2__-tile square grid\nProtection range: __3__\nCollection efficiency: __4__ solar-panel-average-production=Power production (average): __1__ -open-neie-middle-click-hint=Middle mouse button to open Never Enough Items Explorer for this __1__ -tooltip-header-production-recipes=Made with -tooltip-header-miscellaneous-sources=Sources -tooltip-header-consumption-recipes=Needed for +open-neie-middle-click-hint=Middle mouse button to open __YAFC__neie__ for this __1__ +tooltip-header-miscellaneous-sources=Source__plural_for_parameter__1__{1=|rest=s}__ +; English doesn't care about number of items in these lists, but we pass the count for the benefit of languages that might: +tooltip-header-production-recipes=Made with__plural_for_parameter__1__{rest=}__ +tooltip-header-consumption-recipes=Needed for__plural_for_parameter__1__{rest=}__ +; Always only one entity in this list: tooltip-header-item-placement-result=Place result tooltip-header-module-properties=Module parameters productivity-property=Productivity: __1__ @@ -96,18 +100,20 @@ analysis-wastes-useful-products=YAFC analysis: This recipe wastes useful product recipe-uses-fluid-temperature=Uses fluid temperature recipe-uses-mining-productivity=Uses mining productivity recipe-production-scales-with-power=Production scaled with power -tooltip-header-recipe-products=Products -tooltip-header-recipe-crafters=Made in -tooltip-header-allowed-modules=Allowed modules -tooltip-header-unlocked-by-technologies=Unlocked by +; English doesn't care about number of items in several of these lists, but we pass the count for the benefit of languages that might: +tooltip-header-recipe-products=Product__plural_for_parameter__1__{1=|rest=s}__ +tooltip-header-recipe-crafters=Made in__plural_for_parameter__1__{rest=}__ +tooltip-header-allowed-modules=Allowed module__plural_for_parameter__1__{1=|rest=s}__ +tooltip-header-unlocked-by-technologies=Unlocked by__plural_for_parameter__1__{rest=}__ technology-is-disabled=This technology is disabled and cannot be researched. -tooltip-header-technology-prerequisites=Prerequisites +tooltip-header-technology-prerequisites=Prerequisite__plural_for_parameter__1__{1=|rest=s}__ tooltip-header-technology-item-crafting=Item crafting required tooltip-header-technology-capture=Capture __plural_for_parameter__1__{1=this|rest=any}__ entity tooltip-header-technology-mine-entity=Mine __plural_for_parameter__1__{1=this|rest=any}__ entity tooltip-header-technology-build-entity=Build __plural_for_parameter__1__{1=this|rest=any}__ entity -tooltip-header-unlocks-recipes=Unlocks recipes -tooltip-header-unlocks-locations=Unlocks locations +tooltip-header-technology-launch-item=Launch __plural_for_parameter__1__{1=this|rest=any}__ item +tooltip-header-unlocks-recipes=Unlocks recipe__plural_for_parameter__1__{1=|rest=s}__ +tooltip-header-unlocks-locations=Unlocks location__plural_for_parameter__1__{1=|rest=s}__ tooltip-header-total-science-required=Total science required tooltip-quality-upgrade-chance=Upgrade chance: __1__ (multiplied by module bonus) tooltip-header-quality-bonuses=Quality bonuses @@ -121,14 +127,13 @@ tooltip-quality-lightning-attractor=Lightning attractor range & efficiency: quality-bonus-value=+__1__ quality-bonus-value-with-footnote=+__1__* tooltip-quality-module-footnote=* Only applied to beneficial module effects. -tooltip-header-technology-launch-item=Launch __plural_for_parameter__1__{1=this|rest=any}__ item product-suffix-preserved=, preserved until removed from the machine tooltip-entity-spoils-after-no-production=After __1__ of no production, spoils into tooltip-entity-expires-after-no-production=Expires after __1__ of no production tooltip-entity-absorbs-pollution=Absorption: __1__ __2__ per minute tooltip-entity-emits-pollution=Emission: __1__ __2__ per minute tooltip-entity-requires-heat=Requires __1__ heat on cold planets. -item-spoils=After __1__, spoils into +tooltip-item-spoils=After __1__, spoils into ; AboutScreen.cs about-yafc=About YAFC-CE @@ -155,36 +160,41 @@ about-factorio-trademark-disclaimer=Factorio name, content and materials are tra about-factorio-wiki=Documentation on Factorio Wiki ; DependencyExplorer.cs -dependency-fuel=Fuel +dependency-fuel=__plural_for_parameter__1__{1=this|rest=of these}__ Fuel__plural_for_parameter__1__{1=|rest=s}__ +dependency-ingredient=__plural_for_parameter__1__{1=this|rest=of these}__ Ingredient__plural_for_parameter__1__{1=|rest=s}__ +dependency-crafter=__plural_for_parameter__1__{1=this|rest=of these}__ Crafter__plural_for_parameter__1__{1=|rest=s}__ +dependency-source=__plural_for_parameter__1__{1=this|rest=of these}__ Source__plural_for_parameter__1__{1=|rest=s}__ +dependency-technology=__plural_for_parameter__1__{1=this|rest=of these}__ Research__plural_for_parameter__1__{1=|rest=es}__ +dependency-item=Item____plural_for_parameter__1__{1=this|rest=of these}__ plural_for_parameter__1__{1=|rest=s}__ +dependency-location=__plural_for_parameter__1__{1=this|rest=of these}__ Location__plural_for_parameter__1__{1=|rest=s}__ +; __1__ here is the '__1__ is 1' form of one of the above strings: +dependency-require-single=Require __1__: +; __1__ here is the '__1__ is 2 or more' form of one of the above strings: +dependency-require-all=Require ALL __1__: +dependency-require-any=Require ANY __1__: + +; These messages should not end with a period. They get passed as __1__ to dependency-accessible-anyway or dependency-and-not-accessible dependency-fuel-missing=There is no fuel to power this entity -dependency-ingredient=Ingredient dependency-ingredient-missing=There are no ingredients for this recipe dependency-ingredient-variants-missing=There are no ingredient variants for this recipe -dependency-crafter=Crafter dependency-crafter-missing=There are no crafters that can craft this item -dependency-source=Source dependency-sources-missing=This item has no sources -dependency-technology=Research dependency-technology-missing=This recipe is disabled and there are no technologies to unlock it dependency-technology-no-prerequisites=There are no technology prerequisites -dependency-item=Item dependency-item-missing=This entity cannot be placed dependency-map-source-missing=This recipe requires another entity dependency-technology-disabled=This technology is disabled -dependency-location=Location dependency-location-missing=There are no locations that spawn this entity -dependency-require-single=Require this __1__: -dependency-require-all=Require ALL of these __1__s: -dependency-require-any=Require ANY of these __1__s: -; For these two, __1__ is one of the dependency-*-missing messages + dependency-accessible-anyway=__1__, but it is inherently accessible. dependency-and-not-accessible=__1__, and it is inaccessible. -dependency-explorer=Dependency explorer + +dependency-explorer=Dependency Explorer dependency-currently-inspecting=Currently inspecting: dependency-select-something=Select something dependency-click-to-change-hint=(Click to change) dependency-automatable=Status: Automatable -dependency-accessible=Status: Accessible, Not automatable +dependency-accessible=Status: Accessible, not automatable dependency-marked-accessible=Manually marked as accessible dependency-clear-mark=Clear mark dependency-mark-not-accessible=Mark as inaccessible @@ -192,8 +202,8 @@ dependency-mark-accessible-ignoring-milestones=Mark as accessible without milest dependency-marked-not-accessible=Status: Marked as inaccessible dependency-not-accessible=Status: Not accessible. Wrong? dependency-mark-accessible=Manually mark as accessible -dependency-header-dependencies=Dependencies: -dependency-header-dependents=Dependents: +dependency-header-dependencies=Dependenc__plural_for_parameter__1__{1=y|rest=ies}__: +dependency-header-dependents=Dependent__plural_for_parameter__1__{1=|rest=s}__: ; ErrorListPanel.cs error-loading-failed=Loading failed @@ -209,28 +219,24 @@ save-as-png=Save as PNG save-and-open=Save to temp folder and open copied-to-clipboard=Copied to clipboard copy-to-clipboard-with-shortcut=Copy to clipboard (Ctrl+__1__) -save=Save ; MainScreen.cs full-name-with-version=Yet Another Factorio Calculator CE v__1__ create-production-sheet=Create production sheet (Ctrl+__1__) list-and-search-all=List and search all pages (Ctrl+Shift+__1__) -open-neie=Open NEIE +menu-open-neie=Open NEIE search-header=Find on page: undo=Undo shortcut-ctrl-X=Ctrl+__1__ +save=Save save-as=Save As find-on-page=Find on page load-with-same-mods=Load another project (Same mods) return-to-welcome-screen=Return to starting screen menu-header-tools=Tools -milestones=Milestones -menu-preferences=Preferences menu-summary=Summary menu-legacy-summary=Summary (Legacy) -neie=Never Enough Items Explorer -open-dependency-explorer=Open Dependency Explorer -import-from-clipboard=Import page from clipboard +menu-import-from-clipboard=Import page from clipboard menu-header-extra=Extra menu-run-factorio=Run Factorio menu-check-for-updates=Check for updates @@ -246,12 +252,11 @@ close=Close no-newer-version=No newer version running-latest-version=You are running the latest version! network-error=Network error -error-while-checking-for-new-version=There were an error while checking versions. +error-while-checking-for-new-version=There was an error while checking versions. save-project-window-title=Save project save-project-window-header=Save project as load-project-window-title=Load project load-project-window-header=Load another .yafc project -select-project=Select error-critical-loading-exception=Critical loading exception default-file-name=project @@ -270,7 +275,7 @@ search-all-middle-mouse-to-edit-hint=Middle mouse button to edit ; MilestonesEditor.cs milestone-editor=Milestone editor -milestone-description=Hint: You can reorder milestones. When an object is locked behind a milestone, the first inaccessible milestone will be shown. Also when there is a choice between different milestones, first will be chosen +milestone-description=Hint: You can reorder milestones. When an object is locked behind a milestone, the first inaccessible milestone will be shown. Also when there is a choice between different milestones, first will be chosen. milestone-auto-sort=Auto sort milestones milestone-add=Add milestone milestone-add-new=Add new milestone @@ -278,6 +283,7 @@ milestone-cannot-add=Cannot add milestone milestone-cannot-add-already-exists=Milestone already exists ; MilestonesPanel.cs +milestones=Milestones milestones-header=Please select objects that you already have access to: milestones-description=For your convenience, YAFC will show objects you DON'T have access to based on this selection.\nThese are called 'Milestones'. By default all science packs and locations are added as milestones, but this does not have to be this way! You can define your own milestones: Any item, recipe, entity or technology may be added as a milestone. For example you can add advanced electronic circuits as a milestone, and YAFC will display everything that is locked behind those circuits milestones-edit=Edit milestones @@ -285,6 +291,7 @@ milestones-edit-settings=Edit tech progression settings done=Done ; NeverEnoughItemsPanel.cs +neie=Never Enough Items Explorer neie-accepted-variants=Accepted fluid variants neie-building-hours-suffix=__1__bh neie-building-hours-description=Building-hours.\nAmount of building-hours required for all researches assuming crafting speed of 1. @@ -314,11 +321,11 @@ preferences=Preferences preferences-default-belt=Default belt: preferences-default-inserter=Default inserter: preferences-inserter-capacity=Inserter capacity: -preferences-target-technology=Target technology for cost analysis: -preferences-mining-productivity-bonus=Mining productivity bonus: -preferences-research-speed-bonus=Research speed bonus: -preferences-research-productivity-bonus=Research productivity bonus: -preferences-technology-level=__1__ Level: +preferences-target-technology=Target technology for cost analysis: +preferences-mining-productivity-bonus=Mining productivity bonus: +preferences-research-speed-bonus=Research speed bonus: +preferences-research-productivity-bonus=Research productivity bonus: +preferences-technology-level=__1__ Level: prefs-unit-of-time=Unit of time: prefs-unit-seconds=Second prefs-unit-minutes=Minute @@ -359,8 +366,8 @@ delete-page=Delete page export-page-to-clipboard=Share (export string to clipboard) page-settings-screenshot=Make full page screenshot page-settings-export-calculations=Export calculations (to clipboard) -alert-import-page-newer-version=String was created with the newer version of YAFC (__1__). Data may be lost. -alert-import-page-incompatible-version=Share string was created with future version of YAFC (__1__) and is incompatible. +alert-import-page-newer-version=Share string was created with a newer version of YAFC (__1__). Data may be lost. +alert-import-page-incompatible-version=Share string was created with a newer version of YAFC (__1__) and is incompatible. alert-import-page-invalid-string=Clipboard text does not contain valid YAFC share string. import-page-already-exists=Page already exists import-page-already-exists-long=Looks like this page already exists with name '__1__'. Would you like to replace it or import as a copy? @@ -370,7 +377,7 @@ export-no-fuel-selected=No fuel selected export-recipe-disabled=Recipe disabled ; SelectMultiObjectPanel.cs -select-multiple-objects-hint=Hint: ctrl+click to select multiple +select-multiple-objects-hint=Hint: Ctrl+click to select multiple ; SelectObjectPanel.cs type-for-search-hint=Start typing for search @@ -412,9 +419,11 @@ welcome-mod-location-hint=If you don't use separate mod folder, leave it empty ; Possibly translate this as just "Language:"? welcome-language-header=In-game objects language: welcome-load-autosave=Load most recent (auto-)save +welcome-load-autosave-hint=When enabled it will try to find a more recent autosave. Disable if you want to load your manual save only. welcome-use-net-production=Use net production/consumption when analyzing recipes -welcome-software-render-hint=If checked, the main project screen will not use hardware-accelerated rendering.\n\nEnable this setting if YAFC crashes after loading without an error message, or if you know that your computer's graphics hardware does not support modern APIs (e.g. DirectX 12 on Windows). +welcome-use-net-production-hint=If checked, YAFC will only suggest production or consumption recipes that have a net production or consumption of that item or fluid.\nFor example, kovarex enrichment will not be suggested when adding recipes that produce U-238 or consume U-235. welcome-software-render=Force software rendering in project screen +welcome-software-render-hint=If checked, the main project screen will not use hardware-accelerated rendering.\n\nEnable this setting if YAFC crashes after loading without an error message, or if you know that your computer's graphics hardware does not support modern APIs (e.g. DirectX 12 on Windows). recent-projects=Recent projects toggle-dark-mode=Toggle dark mode load-error-create-issue=If all else fails, then create an issue on GitHub @@ -438,6 +447,7 @@ select-folder=Select folder disable-and-reload=Disable & reload disable-and-reload-hint=Disable this mod until you close YAFC or change the mod folder. welcome=Welcome to YAFC CE v__1__ +; Translating these two may be uninteresting; they are only displayed when we cannot yet display the user's desired language: please-wait=Please wait . . . downloading-fonts=YAFC is downloading the fonts for your language. unable-to-load-with-mod=YAFC was unable to load the project. You can disable the problematic mod once by clicking on the '__YAFC__disable-and-reload__' button, or you can disable it permanently for YAFC by copying the mod-folder, disabling the mod in the copy by editing mod-list.json, and pointing YAFC to the copy. @@ -474,8 +484,8 @@ summary-auto-balance=Auto balance progress-running-analysis=Running analysis algorithms ; CostAnalysis.cs -cost-analysis-estimated-amount-for=Estimated amount for __1__: -cost-analysis-estimated-amount=Estimated amount for all researches: +cost-analysis-estimated-amount-for=Estimated amount for __1__: +cost-analysis-estimated-amount=Estimated amount for all researches: cost-analysis-failed=Cost analysis was unable to process this modpack. This may indicate a bug in Yafc. analysis-not-automatable=YAFC analysis: Unable to find a way to fully automate this. cost-analysis-fluid-cost=YAFC cost per 50 units of fluid: ¥__1__ @@ -483,6 +493,7 @@ cost-analysis-item-cost=YAFC cost per item: ¥__1__ cost-analysis-energy-cost=YAFC cost per 1 MW: ¥__1__ cost-analysis-recipe-cost=YAFC cost per recipe: ¥__1__ cost-analysis-generic-cost=YAFC cost: ¥__1__ +; __1__ is one of the above cost-analysis-*-cost strings cost-analysis-with-current-cost=__1__ (Currently ¥__2__) ; DependencyNode.cs @@ -496,7 +507,7 @@ product-amount-range=__1__-__2__x __3__ product-probability=__1__ __2__ product-probability-amount=__1__ __2__x __3__ product-probability-amount-range=__1__ __2__-__3__x __4__ -; Appended to the one of the above +; __1__ is one of the above product strings product-always-fresh=__1__, always fresh product-fixed-spoilage=__1__, __2__ spoiled @@ -516,7 +527,7 @@ format-time-in-minutes=__1__ minutes format-time-in-hours=__1__ hours ; AutoPlanner.cs -auto-planner-missing-goal=Auto planner goal no longer exist +auto-planner-missing-goal=Auto planner goal no longer exists. auto-planner-no-solution=Model has no solution ; ProductionSummary.cs @@ -538,14 +549,14 @@ link-warning-unmatched-nested-link=Nested table link has unmatched production/co link-message-remove-to-link-with-parent=Nested tables have their own set of links that DON'T connect to parent links. To connect this product to the outside, remove this link. link-warning-negative-feedback=YAFC was unable to satisfy this link (Negative feedback loop). This doesn't mean that this link is the problem, but it is part of the loop. link-warning-needs-overproduction=YAFC was unable to satisfy this link (Overproduction). You can allow overproduction for this link to solve the error. -load-error-recipe-does-not-exist=Recipe does not exist -load-error-linked-product-does-not-exist=Linked product does not exist +load-error-recipe-does-not-exist=Recipe does not exist. +load-error-linked-product-does-not-exist=Linked product does not exist. ; Project.cs error-loading-autosave=Fatal error reading the latest autosave. Loading the base file instead. load-error-did-not-read-all-data=Json was not consumed to the end! load-warning-newer-version=This file was created with future YAFC version. This may lose data. -load-error-unable-to-load-file=Unable to load project file +load-error-unable-to-load-file=Unable to load the project file. ; ErrorCollector.cs repeated-error=__1__ (x__2__) @@ -554,7 +565,7 @@ repeated-error=__1__ (x__2__) load-warning-unexpected-object=Project contained an unexpected object. ; SerializationMap.cs -load-error-unable-to-deserialize-untranslated=Unable to deserialize __1__ +load-error-unable-to-deserialize-untranslated=Unable to deserialize __1__. load-error-encountered-unexpected-value=Encountered an unexpected value when reading the project file. ; ValueSerializers.cs @@ -639,12 +650,12 @@ module-customization-filter-buildings=Filter by crafting buildings (Optional): module-customization-add-filter-building=Add module template filter module-customization-enable=Enable custom modules module-customization-internal-modules=Internal modules: -module-customization-leave-zero-hint=Leave zero amount to fill the remaining slots -module-customization-beacons-only=This building doesn't have module slots, but can be affected by beacons +module-customization-leave-zero-hint=Specify zero modules to fill all remaining slots. +module-customization-beacons-only=This building doesn't have module slots, but can be affected by beacons. module-customization-beacon-modules=Beacon modules: module-customization-using-default-beacons=Use default parameters module-customization-override-beacons=Override beacons as well -module-customization-use-number-of-modules-in-beacons=Input the amount of modules, not the amount of beacons. Single beacon can hold __1__ modules. +module-customization-use-number-of-modules-in-beacons=Enter the number of modules, not the number of beacons. A single beacon can hold __1__ module__plural_for_parameter__1__{1=|rest=s}__. module-customization-current-effects=Current effects: module-customization-productivity-bonus=Productivity bonus: __1__ module-customization-speed-bonus=Speed bonus: __1__ (Crafting speed: __2__) @@ -659,7 +670,7 @@ select-beacon=Select beacon select-module=Select module ; ModuleFillerParametersScreen.cs -affected-by-N-beacons=Affected by __1__ +affected-by-M-beacons=Affected by __1__ each-containing-N-modules=each containing __1__ module-filler-remove-current-override-hint=Click here to remove the current override. select-beacon-module=Select beacon module @@ -669,12 +680,12 @@ module-filler-payback-estimate=Modules payback estimate: __1__ module-filler-header-autofill=Module autofill parameters module-filler-fill-miners=Fill modules in miners module-filler-module=Filler module: -module-filler-module-hint=Use this module when autofill doesn't add anything (for example when productivity modules doesn't fit) +module-filler-module-hint=Use this module when autofill doesn't add anything (for example when productivity modules don't fit) module-filler-select-module=Select filler module module-filler-header-beacons=Beacons & beacon modules: module-filler-no-beacons=Your mods contain no beacons, or no modules that can be put into beacons. module-filler-select-beacon-module=Select module for beacon -module-filler-beacons-per-building=Beacons per building: +module-filler-beacons-per-building=Beacons per building: module-filler-beacons-not-calculated=Please note that beacons themselves are not part of the calculation. module-filler-override-beacons=Override beacons: module-filler-override-beacons-hint=Click to change beacon, right-click to change module\nSelect the 'none' item in either prompt to remove the override. @@ -689,15 +700,15 @@ create-new-template-hint=Create new template link-summary-production=Production: __1__ link-summary-implicit-links=Plus additional production from implicit links link-summary-consumption=Consumption: __1__ -link-summary-child-links=Child links: -link-summary-parent-links=Parent links: -link-summary-unrelated-links=Unrelated links: -link-summary-unlinked=Unlinked: +link-summary-child-links=Child links: +link-summary-parent-links=Parent links: +link-summary-unrelated-links=Unrelated links: +link-summary-unlinked=Unlinked: link-summary=Link summary -link-summary-header=Exploring link for: +link-summary-header=Exploring link for: remove-link=Remove link link-summary-no-products=This recipe has no linked products. -link-summary-no-ingredients=This recipe has no linked ingredients +link-summary-no-ingredients=This recipe has no linked ingredients. link-summary-select-product=Select product link to inspect link-summary-select-ingredient=Select ingredient link to inspect link-summary-requested-production=Requested production: __1__ @@ -708,90 +719,84 @@ link-summary-link-nested-under=This link is nested under: link-summary-recipe-nested-under=This recipe is nested under: ; ProductionTableView.cs -production-table-nested-group=This is a nested group. You can drag&drop recipes here. Nested groups can have their own linked materials. production-table-header-recipe=Recipe +production-table-header-entity=Entity +production-table-header-ingredients=Ingredients +production-table-header-products=Products +production-table-header-modules=Modules +production-table-add-raw-recipe=Add raw recipe +production-table-add-technology=Add technology +production-table-export-to-blueprint=Export inputs and outputs to blueprint with constant combinators: +export-blueprint-amount-per=Amount per: +export-blueprint-amount-per-second=second +export-blueprint-amount-per-minute=minute +export-blueprint-amount-per-hour=hour +production-table-remove-zero-building-recipes=Remove all zero-building recipes +production-table-add-technology-hint=Ctrl+click to add a technology instead +production-table-clear-recipes=Clear recipes +production-table-add-all-recipes=Add ALL recipes +production-table-mass-set-assembler=Set assembler for all recipes +production-table-mass-set-quality=Set quality for all recipes +production-table-mass-set-fuel=Set fuel for all recipes +production-table-auto-modules=Auto modules +production-table-module-settings=Module settings +production-table-desired-products=Desired products and amounts (Use negative for input goal): +production-table-add-desired-product=Add desired product +production-table-summary-ingredients=Summary ingredients: +production-table-import-ingredients=Import ingredients: +production-table-extra-products=Extra products: +production-table-export-products=Export products: +production-table-nested-group=This is a nested group. You can drag&drop recipes here. Nested groups can have their own linked materials. + production-table-create-nested=Create nested table production-table-add-nested-product=Add nested desired product production-table-unpack-nested=Unpack nested table -production-table-shortcut-right-click=Shortcut: right-click +production-table-shortcut-right-click=Shortcut: Right-click production-table-shortcut-expand-and-right-click=Shortcut: Expand, then right-click production-table-show-total-io=Show total Input/Output enabled=Enabled add-recipe-to-favorites=Add recipe to favorites +favorite=Favorite production-table-delete-nested=Delete nested table production-table-shortcut-collapse-and-right-click=Shortcut: Collapse, then right-click production-table-delete-recipe=Delete recipe -production-table-export-to-blueprint=Export inputs and outputs to blueprint with constant combinators: -export-blueprint-amount-per=Amount per: -export-blueprint-amount-per-second=second -export-blueprint-amount-per-minute=minute -export-blueprint-amount-per-hour=hour -production-table-remove-zero-building-recipes=Remove all zero-building recipes -production-table-clear-recipes=Clear recipes -production-table-add-all-recipes=Add ALL recipes -production-table-add-raw-recipe=Add raw recipe -production-table-add-technology-hint=Ctrl-click to add a technology instead -select-technology=Select technology -production-table-select-raw-recipe=Select raw recipe -production-table-header-entity=Entity -select-accumulator=Select accumulator -select-crafting-entity=Select crafting entity + +production-table-select-crafting-entity=Select crafting entity +production-table-select-accumulator=Select accumulator production-table-clear-fixed-multiplier=Clear fixed recipe multiplier -production-table-fixed-buildings-hint=Tell YAFC how many buildings it must use when solving this page.\nUse this to ask questions like 'What does it take to handle the output of ten miners?' -production-table-clear-fixed-building-count=Clear fixed building count production-table-set-fixed-building-count=Set fixed building count -production-table-built-building-count-hint=Tell YAFC how many of these buildings you have in your factory.\nYAFC will warn you if you need to build more buildings. -production-table-clear-built-building-count=Clear built building count +production-table-clear-fixed-building-count=Clear fixed building count +production-table-fixed-buildings-hint=Tell YAFC how many buildings it must use when solving this page.\nUse this to ask questions like 'What does it take to handle the output of ten miners?' production-table-set-built-building-count=Set built building count -production-table-generate-building-blueprint-hint=Generate a blueprint for one of these buildings, with the recipe and internal modules set. +production-table-clear-built-building-count=Clear built building count +production-table-built-building-count-hint=Tell YAFC how many of these buildings you have in your factory.\nYAFC will warn you if you need to build more buildings. production-table-generate-building-blueprint=Create single building blueprint +production-table-generate-building-blueprint-hint=Generate a blueprint for one of these buildings, with the recipe and internal modules set. production-table-add-building-to-favorites=Add building to favorites -production-table-mass-set-assembler=Mass set assembler -production-table-select-mass-assembler=Set assembler for all recipes -production-table-mass-set-quality=Mass set quality -production-table-mass-set-fuel=Mass set fuel -production-table-select-mass-fuel=Set fuel for all recipes -production-table-header-ingredients=Ingredients -production-table-header-products=Products -production-table-output-preserved-in-machine=This recipe output does not start spoiling until removed from the machine. -production-table-output-always-fresh=This recipe output is always fresh. -production-table-output-fixed-spoilage=This recipe output is __1__ spoiled. -production-table-header-modules=Modules -production-table-module-template-incompatible=This module template seems incompatible with the recipe or the building -production-table-use-default-modules=Use default modules -production-table-select-modules=Select fixed module -production-table-use-module-template=Use module template: -production-table-configure-module-templates=Configure module templates -production-table-customize-modules=Customize modules -production-table-auto-modules=Auto modules -production-table-module-settings=Module settings -favorite=Favorite -production-table-alert-recipe-exists=Recipe already exists -production-table-query-add-copy=Add a second copy of __1__? -production-table-add-copy=Add a copy + production-table-alert-no-known-fuels=This entity has no known fuels production-table-add-fuel-to-favorites=Add fuel to favorites production-table-select-fuel=Select fuel production-table-accepted-fluids=Accepted fluid variants: -production-table-add-technology=Add technology production-table-add-production-recipe=Add production recipe -production-table-create-new-table=Create new production table for __1__ -production-table-produce-as-spent-fuel=Produce it as a spent fuel +production-table-create-table-for=Create new production table for __1__ +production-table-produce-as-spent-fuel=Produce as spent fuel production-table-add-consumption-recipe=Add consumption recipe production-table-add-fuel-usage=Add fuel usage production-table-add-consumption-technology=Add consumption technology -production-table-add-multiple-hint=Hint: ctrl+click to add multiple +production-table-add-multiple-hint=Hint: Ctrl+click to add multiple production-table-allow-overproduction=Allow overproduction production-table-view-link-summary=View link summary production-table-cannot-unlink=__1__ is a desired product and cannot be unlinked. production-table-currently-linked=__1__ production is currently linked. This means that YAFC will try to match production with consumption. -production-table-remove-desired-product=Remove desired product production-table-remove-and-unlink-desired-product=Remove and unlink +production-table-remove-desired-product=Remove desired product production-table-unlink=Unlink production-table-linked-in-parent=__1__ production is currently linked, but the link is outside this nested table. Nested tables can have their own separate links. +production-table-implicitly-linked=__1__ (__2__) production is implicitly linked. This means that YAFC will use it, along with all other available qualities, to produce __ITEM__science__.\nYou may add a regular link to replace this implicit link. production-table-create-link=Create link production-table-not-linked=__1__ production is currently NOT linked. This means that YAFC will make no attempt to match production with consumption. -production-table-implicitly-linked=__1__ (__2__) production is implicitly linked. This means that YAFC will use it, along with all other available qualities, to produce __3__.\nYou may add a regular link to replace this implicit link. + production-table-set-fixed-fuel=Set fixed fuel consumption production-table-set-fixed-ingredient=Set fixed ingredient consumption production-table-set-fixed-product=Set fixed production amount @@ -801,8 +806,24 @@ production-table-clear-fixed-ingredient=Clear fixed ingredient consumption production-table-clear-fixed-product=Clear fixed production amount production-table-buildings-per-half-belt=(Buildings per half belt: __1__) production-table-inserters-per-building=__1__ (__2__/building) -production-table-approximate-number=~__1__ +production-table-approximate-inserters=~__1__ production-table-approximate-inserters-per-building=~__1__ (__2__/b) + +production-table-output-preserved-in-machine=This recipe output does not start spoiling until removed from the machine. +production-table-output-always-fresh=This recipe output is always fresh. +production-table-output-fixed-spoilage=This recipe output is __1__ spoiled. + +production-table-module-template-incompatible=This module template seems incompatible with the recipe or the building. +production-table-use-default-modules=Use default modules +production-table-select-modules=Select fixed module +production-table-use-module-template=Use module template: +production-table-configure-module-templates=Configure module templates +production-table-customize-modules=Customize modules + +production-table-alert-recipe-exists=Recipe already exists +production-table-query-add-copy=Add a second copy of __1__? +production-table-add-copy=Add a copy + warning-description-deadlock-candidate=Contains recursive links that cannot be matched. No solution exists. warning-description-overproduction-required=This model cannot be solved exactly, it requires some overproduction. You can allow overproduction for any link. This recipe contains one of the possible candidates. warning-description-entity-not-specified=Crafter not specified. Solution is inaccurate. @@ -811,9 +832,9 @@ warning-description-fluid-with-temperature=This recipe uses fuel with temperatur warning-description-fluid-too-hot=Fluid temperature is higher than generator maximum. Some energy is wasted. warning-description-fuel-does-not-provide-energy=This fuel cannot provide any energy to this building. The building won't work. warning-description-has-max-fuel-consumption=This building has max fuel consumption. The rate at which it works is limited by it. -warning-description-ingredient-temperature-range=This recipe does care about ingredient temperature, and the temperature range does not match +warning-description-ingredient-temperature-range=This recipe cares about ingredient temperature, and the temperature range does not match. warning-description-assumes-reactor-formation=Assumes reactor formation from preferences. __YAFC__warning-description-click-for-preferences__ -warning-description-assumes-nauvis-solar=Energy production values assumes Nauvis solar ration (70% power output). Don't forget accumulators. +warning-description-assumes-nauvis-solar=Energy production values assume Nauvis solar ratio (70% power output). Don't forget accumulators. warning-description-needs-more-buildings=This recipe requires more buildings than are currently built. warning-description-asteroid-collectors=The speed of asteroid collectors depends heavily on location and travel speed. It also depends on the distance between adjacent collectors. These dependencies are not modeled. Expect widely varied performance. warning-description-assumes-fulgoran-lightning=Energy production values assume Fulgoran storms and attractors in a square grid.\nThe accumulator estimate tries to store 10% of the energy captured by the attractors. @@ -821,15 +842,9 @@ warning-description-useless-quality=The quality bonus on this recipe has no effe warning-description-excess-productivity-bonus=This building has a larger productivity bonus (from base effect, research, and/or modules) than allowed by the recipe. Please make sure you entered productivity research levels, not percent bonuses. __YAFC__warning-description-click-for-preferences__ warning-description-click-for-preferences=(Click to open the preferences) warning-description-click-for-milestones=(Click to open the milestones window) -production-table-add-desired-product=Add desired product -production-table-desired-products=Desired products and amounts (Use negative for input goal): -production-table-summary-ingredients=Summary ingredients: -production-table-import-ingredients=Import ingredients: -production-table-extra-products=Extra products: -production-table-export-products=Export products: ; ProjectPage.cs default-new-page-name=New page ; ExceptionScreen.cs -ignore-future-errors=Ignore future errors +exception-ignore-future-errors=Ignore future errors diff --git a/Yafc/Widgets/ObjectTooltip.cs b/Yafc/Widgets/ObjectTooltip.cs index f120102e..745b40be 100644 --- a/Yafc/Widgets/ObjectTooltip.cs +++ b/Yafc/Widgets/ObjectTooltip.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Numerics; using Yafc.I18n; using Yafc.Model; using Yafc.UI; @@ -185,12 +186,10 @@ private void BuildCommon(FactorioObject target, ImGui gui) { } if (!target.IsAccessible()) { - string message = LSs.TooltipNotAccessible.L(target.type); - gui.BuildText(message, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.TooltipNotAccessible.L(target.type), TextBlockDisplayStyle.WrappedText); } else if (!target.IsAutomatable()) { - string message = LSs.TooltipNotAutomatable.L(target.type); - gui.BuildText(message, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.TooltipNotAutomatable.L(target.type), TextBlockDisplayStyle.WrappedText); } else { gui.BuildText(CostAnalysis.GetDisplayCost(target), TextBlockDisplayStyle.WrappedText); @@ -258,14 +257,14 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { if (crafter.allowedEffects != AllowedEffects.None) { gui.BuildText(LSs.EntityModuleSlots.L(crafter.moduleSlots)); if (crafter.allowedEffects != AllowedEffects.All) { - gui.BuildText(LSs.AllowedModuleEffectsUntranslatedList.L(crafter.allowedEffects), TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.AllowedModuleEffectsUntranslatedList.L(crafter.allowedEffects, BitOperations.PopCount((uint)crafter.allowedEffects)), TextBlockDisplayStyle.WrappedText); } } } } if (crafter.inputs != null) { - BuildSubHeader(gui, LSs.LabAllowedInputs); + BuildSubHeader(gui, LSs.LabAllowedInputs.L(crafter.inputs)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, crafter.inputs, 2); } @@ -290,7 +289,7 @@ private void BuildEntity(Entity entity, Quality quality, ImGui gui) { if (entity.energy != null) { string energyUsage = EnergyDescriptions[entity.energy.type].L(DataUtils.FormatAmount(entity.Power(quality), UnitOfMeasure.Megawatt)); if (entity.energy.drain > 0f) { - energyUsage = LSs.TooltipAddDrainEnergy.L(energyUsage, DataUtils.FormatAmount(entity.energy.drain, UnitOfMeasure.Megawatt)); + energyUsage = LSs.TooltipActivePlusDrainPower.L(energyUsage, DataUtils.FormatAmount(entity.energy.drain, UnitOfMeasure.Megawatt)); } BuildSubHeader(gui, energyUsage); @@ -370,7 +369,7 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { } if (goods.production.Length > 0) { - BuildSubHeader(gui, LSs.TooltipHeaderProductionRecipes); + BuildSubHeader(gui, LSs.TooltipHeaderProductionRecipes.L(goods.production.Length)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, goods.production, 2); if (tooltipOptions.HintLocations.HasFlag(HintLocations.OnProducingRecipes)) { @@ -381,14 +380,14 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { } if (goods.miscSources.Length > 0) { - BuildSubHeader(gui, LSs.TooltipHeaderMiscellaneousSources); + BuildSubHeader(gui, LSs.TooltipHeaderMiscellaneousSources.L(goods.miscSources.Length)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, goods.miscSources, 2); } } if (goods.usages.Length > 0) { - BuildSubHeader(gui, LSs.TooltipHeaderConsumptionRecipes); + BuildSubHeader(gui, LSs.TooltipHeaderConsumptionRecipes.L(goods.usages.Length)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, goods.usages, 4); if (tooltipOptions.HintLocations.HasFlag(HintLocations.OnConsumingRecipes)) { @@ -402,7 +401,7 @@ private void BuildGoods(Goods goods, Quality quality, ImGui gui) { BuildSubHeader(gui, LSs.Perishable); using (gui.EnterGroup(contentPadding)) { float spoilTime = perishable.GetSpoilTime(quality) / Project.current.settings.spoilingRate; - gui.BuildText(LSs.ItemSpoils.L(DataUtils.FormatTime(spoilTime))); + gui.BuildText(LSs.TooltipItemSpoils.L(DataUtils.FormatTime(spoilTime))); gui.BuildFactorioObjectButtonWithText(spoiled, iconDisplayStyle: IconDisplayStyle.Default with { AlwaysAccessible = true }); tooltipOptions.ExtraSpoilInformation?.Invoke(gui); } @@ -513,7 +512,7 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { } if (recipe is Recipe { products.Length: > 0 } && !(recipe.products.Length == 1 && recipe.products[0].IsSimple)) { - BuildSubHeader(gui, LSs.TooltipHeaderRecipeProducts); + BuildSubHeader(gui, LSs.TooltipHeaderRecipeProducts.L(recipe.products.Length)); using (gui.EnterGroup(contentPadding)) { string? extraText = recipe is Recipe { preserveProducts: true } ? LSs.ProductSuffixPreserved : null; foreach (var product in recipe.products) { @@ -522,7 +521,7 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { } } - BuildSubHeader(gui, LSs.TooltipHeaderRecipeCrafters); + BuildSubHeader(gui, LSs.TooltipHeaderRecipeCrafters.L(recipe.crafters.Length)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, recipe.crafters, 2); } @@ -530,7 +529,7 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { List allowedModules = [.. Database.allModules.Where(recipe.CanAcceptModule)]; if (allowedModules.Count > 0) { - BuildSubHeader(gui, LSs.TooltipHeaderAllowedModules); + BuildSubHeader(gui, LSs.TooltipHeaderAllowedModules.L(allowedModules.Count)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, allowedModules, 1); } @@ -554,7 +553,7 @@ private static void BuildRecipe(RecipeOrTechnology recipe, ImGui gui) { } if (recipe is Recipe lockedRecipe && !lockedRecipe.enabled) { - BuildSubHeader(gui, LSs.TooltipHeaderUnlockedByTechnologies); + BuildSubHeader(gui, LSs.TooltipHeaderUnlockedByTechnologies.L(lockedRecipe.technologyUnlock.Length)); using (gui.EnterGroup(contentPadding)) { if (lockedRecipe.technologyUnlock.Length > 2) { BuildIconRow(gui, lockedRecipe.technologyUnlock, 1); @@ -597,7 +596,7 @@ private static void BuildTechnology(Technology technology, ImGui gui) { } if (technology.prerequisites.Length > 0) { - BuildSubHeader(gui, LSs.TooltipHeaderTechnologyPrerequisites); + BuildSubHeader(gui, LSs.TooltipHeaderTechnologyPrerequisites.L(technology.prerequisites.Length)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.prerequisites, 1); } @@ -644,14 +643,14 @@ private static void BuildTechnology(Technology technology, ImGui gui) { } if (technology.unlockRecipes.Count > 0) { - BuildSubHeader(gui, LSs.TooltipHeaderUnlocksRecipes); + BuildSubHeader(gui, LSs.TooltipHeaderUnlocksRecipes.L(technology.unlockRecipes.Count)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.unlockRecipes, 2); } } if (technology.unlockLocations.Count > 0) { - BuildSubHeader(gui, LSs.TooltipHeaderUnlocksLocations); + BuildSubHeader(gui, LSs.TooltipHeaderUnlocksLocations.L(technology.unlockLocations.Count)); using (gui.EnterGroup(contentPadding)) { BuildIconRow(gui, technology.unlockLocations, 2); } diff --git a/Yafc/Windows/DependencyExplorer.cs b/Yafc/Windows/DependencyExplorer.cs index 5f55ee51..dc218a43 100644 --- a/Yafc/Windows/DependencyExplorer.cs +++ b/Yafc/Windows/DependencyExplorer.cs @@ -15,7 +15,7 @@ public class DependencyExplorer : PseudoScreen { private readonly List history = []; private FactorioObject current; - private static readonly Dictionary dependencyListTexts = new Dictionary() + private static readonly Dictionary dependencyListTexts = new() { {DependencyNode.Flags.Fuel, (LSs.DependencyFuel, LSs.DependencyFuelMissing)}, {DependencyNode.Flags.Ingredient, (LSs.DependencyIngredient, LSs.DependencyIngredientMissing)}, @@ -26,7 +26,7 @@ public class DependencyExplorer : PseudoScreen { {DependencyNode.Flags.TechnologyPrerequisites, (LSs.DependencyTechnology, LSs.DependencyTechnologyNoPrerequisites)}, {DependencyNode.Flags.ItemToPlace, (LSs.DependencyItem, LSs.DependencyItemMissing)}, {DependencyNode.Flags.SourceEntity, (LSs.DependencySource, LSs.DependencyMapSourceMissing)}, - {DependencyNode.Flags.Disabled, ("", LSs.DependencyTechnologyDisabled)}, + {DependencyNode.Flags.Disabled, (null, LSs.DependencyTechnologyDisabled)}, {DependencyNode.Flags.Location, (LSs.DependencyLocation, LSs.DependencyLocationMissing)}, }; @@ -54,20 +54,27 @@ private void DrawDependencies(ImGui gui) { gui.spacing = 0f; Dependencies.dependencyList[current].Draw(gui, (gui, elements, flags) => { - if (!dependencyListTexts.TryGetValue(flags, out var dependencyType)) { - dependencyType = (flags.ToString(), "Missing " + flags); + string name; + string missingText; + if (dependencyListTexts.TryGetValue(flags, out var dependencyType)) { + name = dependencyType.name?.L(elements.Count) ?? ""; + missingText = dependencyType.missingText; + } + else { + name = flags.ToString(); + missingText = "Missing " + flags; } if (elements.Count > 0) { gui.AllocateSpacing(0.5f); if (elements.Count == 1) { - gui.BuildText(LSs.DependencyRequireSingle.L(dependencyType.name)); + gui.BuildText(LSs.DependencyRequireSingle.L(name)); } else if (flags.HasFlags(DependencyNode.Flags.RequireEverything)) { - gui.BuildText(LSs.DependencyRequireAll.L(dependencyType.name)); + gui.BuildText(LSs.DependencyRequireAll.L(name)); } else { - gui.BuildText(LSs.DependencyRequireAny.L(dependencyType.name)); + gui.BuildText(LSs.DependencyRequireAny.L(name)); } gui.AllocateSpacing(0.5f); @@ -76,15 +83,14 @@ private void DrawDependencies(ImGui gui) { } } else { - string text = dependencyType.missingText; if (Database.rootAccessible.Contains(current)) { - text = LSs.DependencyAccessibleAnyway.L(text); + missingText = LSs.DependencyAccessibleAnyway.L(missingText); } else { - text = LSs.DependencyAndNotAccessible.L(text); + missingText = LSs.DependencyAndNotAccessible.L(missingText); } - gui.BuildText(text, TextBlockDisplayStyle.WrappedText); + gui.BuildText(missingText, TextBlockDisplayStyle.WrappedText); } }); } @@ -164,10 +170,10 @@ public override void Build(ImGui gui) { gui.AllocateSpacing(2f); using var split = gui.EnterHorizontalSplit(2); split.Next(); - gui.BuildText(LSs.DependencyHeaderDependencies, Font.subheader); + gui.BuildText(LSs.DependencyHeaderDependencies.L(Dependencies.dependencyList[current].Count()), Font.subheader); dependencies.Build(gui); split.Next(); - gui.BuildText(LSs.DependencyHeaderDependents, Font.subheader); + gui.BuildText(LSs.DependencyHeaderDependents.L(Dependencies.reverseDependencies[current].Count), Font.subheader); dependents.Build(gui); } diff --git a/Yafc/Windows/MainScreen.cs b/Yafc/Windows/MainScreen.cs index 4bff3878..8ce82fc3 100644 --- a/Yafc/Windows/MainScreen.cs +++ b/Yafc/Windows/MainScreen.cs @@ -329,7 +329,7 @@ public static void BuildSubHeader(ImGui gui, string text) { } } - private static void ShowNeie() => SelectSingleObjectPanel.Select(Database.goods.explorable, LSs.OpenNeie, NeverEnoughItemsPanel.Show); + private static void ShowNeie() => SelectSingleObjectPanel.Select(Database.goods.explorable, LSs.MenuOpenNeie, NeverEnoughItemsPanel.Show); private void SetSearch(SearchQuery searchQuery) { pageSearch = searchQuery; @@ -395,7 +395,7 @@ private void SettingsDropdown(ImGui gui) { _ = ShowPseudoScreen(new MilestonesPanel()); } - if (gui.BuildContextMenuButton(LSs.MenuPreferences) && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.Preferences) && gui.CloseDropdown()) { PreferencesScreen.ShowPreviousState(); } @@ -412,10 +412,10 @@ private void SettingsDropdown(ImGui gui) { } if (gui.BuildContextMenuButton(LSs.DependencyExplorer) && gui.CloseDropdown()) { - SelectSingleObjectPanel.Select(Database.objects.explorable, LSs.OpenDependencyExplorer, DependencyExplorer.Show); + SelectSingleObjectPanel.Select(Database.objects.explorable, LSs.DependencyExplorer, DependencyExplorer.Show); } - if (gui.BuildContextMenuButton(LSs.ImportFromClipboard, disabled: !ImGuiUtils.HasClipboardText()) && gui.CloseDropdown()) { + if (gui.BuildContextMenuButton(LSs.MenuImportFromClipboard, disabled: !ImGuiUtils.HasClipboardText()) && gui.CloseDropdown()) { ProjectPageSettingsPanel.LoadProjectPageFromClipboard(); } @@ -665,7 +665,7 @@ private async void LoadProjectLight() { } string? projectDirectory = string.IsNullOrEmpty(project.attachedFileName) ? null : Path.GetDirectoryName(project.attachedFileName); - string? path = await new FilesystemScreen(LSs.LoadProjectWindowTitle, LSs.LoadProjectWindowHeader, LSs.SelectProject, projectDirectory, + string? path = await new FilesystemScreen(LSs.LoadProjectWindowTitle, LSs.LoadProjectWindowHeader, LSs.Select, projectDirectory, FilesystemScreen.Mode.SelectOrCreateFile, LSs.DefaultFileName, this, null, "yafc"); if (path == null) { diff --git a/Yafc/Windows/MilestonesEditor.cs b/Yafc/Windows/MilestonesEditor.cs index 9b8942c5..80eddcf6 100644 --- a/Yafc/Windows/MilestonesEditor.cs +++ b/Yafc/Windows/MilestonesEditor.cs @@ -48,8 +48,7 @@ public override void Build(ImGui gui) { BuildHeader(gui, LSs.MilestoneEditor); milestoneList.Build(gui); - string milestoneHintText = LSs.MilestoneDescription; - gui.BuildText(milestoneHintText, TextBlockDisplayStyle.WrappedText with { Color = SchemeColor.BackgroundTextFaint }); + gui.BuildText(LSs.MilestoneDescription, TextBlockDisplayStyle.WrappedText with { Color = SchemeColor.BackgroundTextFaint }); using (gui.EnterRow()) { if (gui.BuildButton(LSs.MilestoneAutoSort, SchemeColor.Grey)) { diff --git a/Yafc/Windows/PreferencesScreen.cs b/Yafc/Windows/PreferencesScreen.cs index dd5bb7f9..db35ffdd 100644 --- a/Yafc/Windows/PreferencesScreen.cs +++ b/Yafc/Windows/PreferencesScreen.cs @@ -149,9 +149,7 @@ private static void DrawGeneral(ImGui gui) { } } - string iconScaleMessage = LSs.PrefsIconScaleHint; - - using (gui.EnterRowWithHelpIcon(iconScaleMessage)) { + using (gui.EnterRowWithHelpIcon(LSs.PrefsIconScaleHint)) { gui.BuildText(LSs.PrefsIconScale, topOffset: 0.5f); DisplayAmount amount = new(preferences.iconScale, UnitOfMeasure.Percent); if (gui.BuildFloatInput(amount, TextBoxDisplayStyle.DefaultTextInput) && amount.Value > 0 && amount.Value <= 1) { @@ -163,8 +161,7 @@ private static void DrawGeneral(ImGui gui) { // Don't show this preference if it isn't relevant. // (Takes ~3ms for pY, which would concern me in the regular UI, but should be fine here.) if (Database.objects.all.Any(o => Milestones.Instance.GetMilestoneResult(o).PopCount() > 22)) { - string overlapMessage = LSs.PrefsMilestonesPerLineHint; - using (gui.EnterRowWithHelpIcon(overlapMessage)) { + using (gui.EnterRowWithHelpIcon(LSs.PrefsMilestonesPerLineHint)) { gui.BuildText(LSs.PrefsMilestonesPerLine, topOffset: 0.5f); if (gui.BuildIntegerInput(preferences.maxMilestonesPerTooltipLine, out int newIntValue) && newIntValue >= 22) { preferences.RecordUndo().maxMilestonesPerTooltipLine = newIntValue; diff --git a/Yafc/Windows/WelcomeScreen.cs b/Yafc/Windows/WelcomeScreen.cs index 1eb2fc96..c49a9585 100644 --- a/Yafc/Windows/WelcomeScreen.cs +++ b/Yafc/Windows/WelcomeScreen.cs @@ -219,7 +219,7 @@ protected override void BuildContents(ImGui gui) { gui.BuildText(LSs.WelcomeLanguageHeader); } - using (gui.EnterRowWithHelpIcon("""When enabled it will try to find a more recent autosave. Disable if you want to load your manual save only.""", false)) { + using (gui.EnterRowWithHelpIcon(LSs.WelcomeLoadAutosaveHint, false)) { if (gui.BuildCheckBox(LSs.WelcomeLoadAutosave, Preferences.Instance.useMostRecentSave, out useMostRecentSave)) { Preferences.Instance.useMostRecentSave = useMostRecentSave; @@ -227,16 +227,11 @@ protected override void BuildContents(ImGui gui) { } } - using (gui.EnterRowWithHelpIcon(""" - If checked, YAFC will only suggest production or consumption recipes that have a net production or consumption of that item or fluid. - For example, kovarex enrichment will not be suggested when adding recipes that produce U-238 or consume U-235. - """, false)) { + using (gui.EnterRowWithHelpIcon(LSs.WelcomeUseNetProductionHint, false)) { _ = gui.BuildCheckBox(LSs.WelcomeUseNetProduction, netProduction, out netProduction); } - string softwareRenderHint = LSs.WelcomeSoftwareRenderHint; - - using (gui.EnterRowWithHelpIcon(softwareRenderHint, false)) { + using (gui.EnterRowWithHelpIcon(LSs.WelcomeSoftwareRenderHint, false)) { bool forceSoftwareRenderer = Preferences.Instance.forceSoftwareRenderer; _ = gui.BuildCheckBox(LSs.WelcomeSoftwareRender, forceSoftwareRenderer, out forceSoftwareRenderer); @@ -365,7 +360,7 @@ private void LanguageSelection(ImGui gui) { if (!Program.hasOverriddenFont) { gui.AllocateSpacing(0.5f); - gui.BuildText((string)LSs.WelcomeAlertNeedADifferentFont, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.WelcomeAlertNeedADifferentFont, TextBlockDisplayStyle.WrappedText); gui.AllocateSpacing(0.5f); } DoLanguageList(gui, languageMapping, false); diff --git a/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs b/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs index 160dbc8d..328101c7 100644 --- a/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs +++ b/Yafc/Workspace/ProductionTable/ModuleFillerParametersScreen.cs @@ -32,10 +32,10 @@ private void ListDrawer(ImGui gui, KeyValuePair { using (gui.EnterRow()) { // Allocate the width now, but draw the text later so it can be vertically centered. - Rect rect = gui.AllocateTextRect(out _, LSs.AffectedByNBeacons.L(element.Value.beaconCount), TextBlockDisplayStyle.Default(SchemeColor.None)); + Rect rect = gui.AllocateTextRect(out _, LSs.AffectedByMBeacons.L(element.Value.beaconCount), TextBlockDisplayStyle.Default(SchemeColor.None)); gui.BuildFactorioObjectIcon(element.Value.beacon, ButtonDisplayStyle.ProductionTableUnscaled); rect.Height = gui.lastRect.Height; - gui.DrawText(rect, LSs.AffectedByNBeacons.L(element.Value.beaconCount)); + gui.DrawText(rect, LSs.AffectedByMBeacons.L(element.Value.beaconCount)); gui.BuildText(LSs.EachContainingNModules.L(element.Value.beacon.target.moduleSlots)); gui.BuildFactorioObjectIcon(element.Value.beaconModule, ButtonDisplayStyle.ProductionTableUnscaled); } diff --git a/Yafc/Workspace/ProductionTable/ProductionTableView.cs b/Yafc/Workspace/ProductionTable/ProductionTableView.cs index ad7aa01a..64785b67 100644 --- a/Yafc/Workspace/ProductionTable/ProductionTableView.cs +++ b/Yafc/Workspace/ProductionTable/ProductionTableView.cs @@ -269,12 +269,12 @@ public override void BuildMenu(ImGui gui) { private static void BuildRecipeButton(ImGui gui, ProductionTable table) { if (gui.BuildButton(LSs.ProductionTableAddRawRecipe).WithTooltip(gui, LSs.ProductionTableAddTechnologyHint) && gui.CloseDropdown()) { if (InputSystem.Instance.control) { - SelectMultiObjectPanel.Select(Database.technologies.all, LSs.SelectTechnology, + SelectMultiObjectPanel.Select(Database.technologies.all, LSs.ProductionTableAddTechnology, r => table.AddRecipe(r.With(Quality.Normal), DefaultVariantOrdering), checkMark: r => table.recipes.Any(rr => rr.recipe.target == r), yellowMark: r => table.GetAllRecipes().Any(rr => rr.recipe.target == r)); } else { var prodTable = ProductionLinkSummaryScreen.FindProductionTable(table, out List parents); - SelectMultiObjectPanel.SelectWithQuality(Database.recipes.explorable.AsEnumerable(), LSs.ProductionTableSelectRawRecipe, + SelectMultiObjectPanel.SelectWithQuality(Database.recipes.explorable.AsEnumerable(), LSs.ProductionTableAddRawRecipe, r => table.AddRecipe(r, DefaultVariantOrdering), Quality.Normal, checkMark: r => table.recipes.Any(rr => rr.recipe.target == r), yellowMark: r => prodTable?.GetAllRecipes().Any(rr => rr.recipe.target == r) ?? false); } } @@ -446,7 +446,7 @@ private static void BuildAccumulatorView(ImGui gui, RecipeRow recipe) { private static void ShowAccumulatorDropdown(ImGui gui, RecipeRow recipe, Entity currentAccumulator, Quality accumulatorQuality) => gui.BuildObjectQualitySelectDropDown(Database.allAccumulators, newAccumulator => recipe.RecordUndo().ChangeVariant(currentAccumulator, newAccumulator.target), - new(LSs.SelectAccumulator, ExtraText: x => DataUtils.FormatAmount(x.AccumulatorCapacity(accumulatorQuality), UnitOfMeasure.Megajoule)), + new(LSs.ProductionTableSelectAccumulator, ExtraText: x => DataUtils.FormatAmount(x.AccumulatorCapacity(accumulatorQuality), UnitOfMeasure.Megajoule)), accumulatorQuality, newQuality => recipe.RecordUndo().ChangeVariant(accumulatorQuality, newQuality)); @@ -477,7 +477,7 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { if (!sel.energy.fuels.Contains(recipe.fuel?.target)) { recipe.fuel = recipe.entity.target.energy.fuels.AutoSelect(DataUtils.FavoriteFuel).With(Quality.Normal); } - }, new(LSs.SelectCraftingEntity, DataUtils.FavoriteCrafter, ExtraText: x => DataUtils.FormatAmount(x.CraftingSpeed(quality), UnitOfMeasure.Percent))); + }, new(LSs.ProductionTableSelectCraftingEntity, DataUtils.FavoriteCrafter, ExtraText: x => DataUtils.FormatAmount(x.CraftingSpeed(quality), UnitOfMeasure.Percent))); gui.AllocateSpacing(0.5f); @@ -492,9 +492,7 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { } if (recipe.hierarchyEnabled) { - string fixedBuildingsTip = LSs.ProductionTableFixedBuildingsHint; - - using (gui.EnterRowWithHelpIcon(fixedBuildingsTip)) { + using (gui.EnterRowWithHelpIcon(LSs.ProductionTableFixedBuildingsHint)) { gui.allocator = RectAllocator.RemainingRow; if (recipe.fixedBuildings > 0f && !recipe.fixedFuel && recipe.fixedIngredient == null && recipe.fixedProduct == null) { ButtonEvent evt = gui.BuildButton(LSs.ProductionTableClearFixedBuildingCount); @@ -576,7 +574,7 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) { public override void BuildMenu(ImGui gui) { if (gui.BuildButton(LSs.ProductionTableMassSetAssembler) && gui.CloseDropdown()) { - SelectSingleObjectPanel.Select(Database.allCrafters, LSs.ProductionTableSelectMassAssembler, set => { + SelectSingleObjectPanel.Select(Database.allCrafters, LSs.ProductionTableMassSetAssembler, set => { DataUtils.FavoriteCrafter.AddToFavorite(set, 10); foreach (var recipe in view.GetRecipesRecursive()) { @@ -599,7 +597,7 @@ public override void BuildMenu(ImGui gui) { } if (gui.BuildButton(LSs.ProductionTableMassSetFuel) && gui.CloseDropdown()) { - SelectSingleObjectPanel.SelectWithQuality(Database.goods.all.Where(x => x.fuelValue > 0), LSs.ProductionTableSelectMassFuel, set => { + SelectSingleObjectPanel.SelectWithQuality(Database.goods.all.Where(x => x.fuelValue > 0), LSs.ProductionTableMassSetFuel, set => { DataUtils.FavoriteFuel.AddToFavorite(set.target, 10); foreach (var recipe in view.GetRecipesRecursive()) { @@ -991,7 +989,7 @@ void dropDownContent(ImGui gui) { if (goods == Database.science) { if (gui.BuildButton(LSs.ProductionTableAddTechnology) && gui.CloseDropdown()) { - SelectMultiObjectPanel.Select(Database.technologies.all, LSs.SelectTechnology, + SelectMultiObjectPanel.Select(Database.technologies.all, LSs.ProductionTableAddTechnology, r => context.AddRecipe(r.With(Quality.Normal), DefaultVariantOrdering), checkMark: r => context.recipes.Any(rr => rr.recipe.target == r)); } } @@ -1008,7 +1006,7 @@ void dropDownContent(ImGui gui) { CreateNewProductionTable(goods, amount); } else if (evt == ButtonEvent.MouseOver) { - gui.ShowTooltip(iconRect, LSs.ProductionTableCreateNewTable.L(goods.target.locName)); + gui.ShowTooltip(iconRect, LSs.ProductionTableCreateTableFor.L(goods.target.locName)); } } } @@ -1085,8 +1083,7 @@ void dropDownContent(ImGui gui) { gui.BuildText(LSs.ProductionTableCannotUnlink.L(goods.target.locName), TextBlockDisplayStyle.WrappedText); } else { - string goodProdLinkedMessage = LSs.ProductionTableCurrentlyLinked.L(goods.target.locName); - gui.BuildText(goodProdLinkedMessage, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ProductionTableCurrentlyLinked.L(goods.target.locName), TextBlockDisplayStyle.WrappedText); } if (type is ProductDropdownType.DesiredIngredient or ProductDropdownType.DesiredProduct) { @@ -1104,19 +1101,17 @@ void dropDownContent(ImGui gui) { } else if (goods != null) { if (link != null) { - string goodsNestLinkMessage = LSs.ProductionTableLinkedInParent.L(goods.target.locName); - gui.BuildText(goodsNestLinkMessage, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ProductionTableLinkedInParent.L(goods.target.locName), TextBlockDisplayStyle.WrappedText); } else if (iLink != null) { - string implicitLink = LSs.ProductionTableImplicitlyLinked.L(goods.target.locName, goods.quality.locName, Database.science.target.locName); + string implicitLink = LSs.ProductionTableImplicitlyLinked.L(goods.target.locName, goods.quality.locName); gui.BuildText(implicitLink, TextBlockDisplayStyle.WrappedText); if (gui.BuildButton(LSs.ProductionTableCreateLink).WithTooltip(gui, LSs.ProductionTableShortcutRightClick) && gui.CloseDropdown()) { CreateLink(context, goods); } } else if (goods.target.isLinkable) { - string notLinkedMessage = LSs.ProductionTableNotLinked.L(goods.target.locName); - gui.BuildText(notLinkedMessage, TextBlockDisplayStyle.WrappedText); + gui.BuildText(LSs.ProductionTableNotLinked.L(goods.target.locName), TextBlockDisplayStyle.WrappedText); if (gui.BuildButton(LSs.ProductionTableCreateLink).WithTooltip(gui, LSs.ProductionTableShortcutRightClick) && gui.CloseDropdown()) { CreateLink(context, goods); } @@ -1448,7 +1443,7 @@ private static void BuildBeltInserterInfo(ImGui gui, Item item, float amount, fl click |= gui.BuildFactorioObjectButton(belt, ButtonDisplayStyle.Default) == Click.Left; gui.AllocateSpacing(-1.5f); click |= gui.BuildFactorioObjectButton(inserter, ButtonDisplayStyle.Default) == Click.Left; - text = LSs.ProductionTableApproximateNumber.L(DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None)); + text = LSs.ProductionTableApproximateInserters.L(DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None)); if (buildingCount > 1) { text = LSs.ProductionTableApproximateInsertersPerBuilding.L(DataUtils.FormatAmount(inserterToBelt, UnitOfMeasure.None), diff --git a/changelog.txt b/changelog.txt index 623f360c..c0d65faf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,7 @@ Date: Features: - When changing the language for Factorio objects, automatically switch to, or download, a font that can display that language. + - All of YAFC can be translated, not just the strings that are used in Factorio. Fixes: - Prevent productivity bonuses from exceeding +300%, unless otherwise allowed by mods. - When loading duplicate research productivity effects, obey both of them, instead of failing. From de5cb6c155b170b1f843d2cefe52d717f58df03b Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 14:22:54 -0400 Subject: [PATCH 34/54] test crowdin integration --- .github/workflows/crowdin-translation-action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index 57a2eca0..47537f3d 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -2,7 +2,7 @@ name: Crowdin Action on: push: - branches: [ main ] + branches: [ internationalization-test ] jobs: synchronize-with-crowdin: @@ -22,7 +22,7 @@ jobs: create_pull_request: true pull_request_title: 'New Crowdin Translations' pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' - pull_request_base_branch_name: 'main' + pull_request_base_branch_name: 'internationalization-test' env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 2e1d327214c69140dfd794b09424755711d6f021 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 14:31:07 -0400 Subject: [PATCH 35/54] . --- .github/workflows/crowdin-translation-action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index 47537f3d..e8849350 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -23,6 +23,7 @@ jobs: pull_request_title: 'New Crowdin Translations' pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' + source_paths: 'yafc/Data/locale/en/*' env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From be5384421cb7a552f770499e693594ff3332876c Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 14:36:49 -0400 Subject: [PATCH 36/54] . --- .github/workflows/crowdin-translation-action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index e8849350..64c1018d 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -23,7 +23,8 @@ jobs: pull_request_title: 'New Crowdin Translations' pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' - source_paths: 'yafc/Data/locale/en/*' + source: 'yafc/Data/locale/en/yafc.cfg' + translation: 'yafc/Data/locale/%two_letters_code%.cfg' env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 652ed7cd7e11648adcc02ceeaaa1cab3491f8e72 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 14:38:17 -0400 Subject: [PATCH 37/54] . --- .github/workflows/crowdin-translation-action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index 64c1018d..6319b05f 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -23,8 +23,8 @@ jobs: pull_request_title: 'New Crowdin Translations' pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' - source: 'yafc/Data/locale/en/yafc.cfg' - translation: 'yafc/Data/locale/%two_letters_code%.cfg' + source: 'Yafc/Data/locale/en/yafc.cfg' + translation: 'Yafc/Data/locale/%two_letters_code%.cfg' env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 4cf21fbb5101d3b872ed10649a0f109fcec1d8b7 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:00:18 -0400 Subject: [PATCH 38/54] . --- .github/workflows/crowdin-translation-action.yml | 3 +-- crowdin.yml | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index 6319b05f..9766a9a8 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -23,8 +23,7 @@ jobs: pull_request_title: 'New Crowdin Translations' pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' - source: 'Yafc/Data/locale/en/yafc.cfg' - translation: 'Yafc/Data/locale/%two_letters_code%.cfg' + skip_untranslated_files: true env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/crowdin.yml b/crowdin.yml index 8f901685..8f07e286 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -13,7 +13,8 @@ "files": [ { - "source": "locales/en.yml", - "translation": "locales/%two_letters_code%.yml" + "source": "Yafc/Data/locale/en/Yafc.cfg", + "dest": "Yafc/Data/locale/en/Yafc.ini", + "translation": "Yafc/Data/locale/%two_letters_code%/Yafc.cfg" } ] From 954e4554a4a44cfdf6a020259ee170e56a012545 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:01:58 -0400 Subject: [PATCH 39/54] . --- crowdin.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crowdin.yml b/crowdin.yml index 8f07e286..89b1b27b 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -13,8 +13,8 @@ "files": [ { - "source": "Yafc/Data/locale/en/Yafc.cfg", - "dest": "Yafc/Data/locale/en/Yafc.ini", - "translation": "Yafc/Data/locale/%two_letters_code%/Yafc.cfg" + "source": "Yafc/Data/locale/en/yafc.cfg", + "dest": "Yafc/Data/locale/en/yafc.ini", + "translation": "Yafc/Data/locale/%two_letters_code%/yafc.cfg" } ] From 11c4f876b211d71a18ea023d5c00141cccd492e1 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:04:53 -0400 Subject: [PATCH 40/54] . --- crowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdin.yml b/crowdin.yml index 89b1b27b..6940499a 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -14,7 +14,7 @@ "files": [ { "source": "Yafc/Data/locale/en/yafc.cfg", - "dest": "Yafc/Data/locale/en/yafc.ini", + "dest": "yafc.ini", "translation": "Yafc/Data/locale/%two_letters_code%/yafc.cfg" } ] From fc963fa05cdbd6a404353594b0e08bbc13369daf Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:08:38 -0400 Subject: [PATCH 41/54] . --- .github/workflows/crowdin-translation-action.yml | 1 - crowdin.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index 9766a9a8..47537f3d 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -23,7 +23,6 @@ jobs: pull_request_title: 'New Crowdin Translations' pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' - skip_untranslated_files: true env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/crowdin.yml b/crowdin.yml index 6940499a..7d3e05fc 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -15,6 +15,6 @@ { "source": "Yafc/Data/locale/en/yafc.cfg", "dest": "yafc.ini", - "translation": "Yafc/Data/locale/%two_letters_code%/yafc.cfg" + "translation": "Yafc/Data/locale/%locale%/yafc.cfg" } ] From 5273724b933c62dd1c2264c5ab883505f2d20c61 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:14:21 -0400 Subject: [PATCH 42/54] . --- crowdin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crowdin.yml b/crowdin.yml index 7d3e05fc..6940499a 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -15,6 +15,6 @@ { "source": "Yafc/Data/locale/en/yafc.cfg", "dest": "yafc.ini", - "translation": "Yafc/Data/locale/%locale%/yafc.cfg" + "translation": "Yafc/Data/locale/%two_letters_code%/yafc.cfg" } ] From 92013b92be0a99a0a2221b9a086923fb0d4c8b07 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:33:23 -0400 Subject: [PATCH 43/54] . --- .github/workflows/crowdin-translation-action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index 47537f3d..1999e30c 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -23,6 +23,7 @@ jobs: pull_request_title: 'New Crowdin Translations' pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' + skip_untranslated_strings: true env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From d7ced826856eb5a395f6df12a95e9dc6723a8dd1 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:37:47 -0400 Subject: [PATCH 44/54] . --- .github/workflows/crowdin-translation-action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index 1999e30c..a7f80fb2 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -24,6 +24,7 @@ jobs: pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' skip_untranslated_strings: true + skip_untranslated_files: true env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 2d7eadad825d80cf7fa6fa673c949381d706d19e Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:39:43 -0400 Subject: [PATCH 45/54] . --- .github/workflows/crowdin-translation-action.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/crowdin-translation-action.yml b/.github/workflows/crowdin-translation-action.yml index a7f80fb2..1999e30c 100644 --- a/.github/workflows/crowdin-translation-action.yml +++ b/.github/workflows/crowdin-translation-action.yml @@ -24,7 +24,6 @@ jobs: pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' pull_request_base_branch_name: 'internationalization-test' skip_untranslated_strings: true - skip_untranslated_files: true env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} From 03852ded0d8de08c31fe887092aacf9d1cd23ecc Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:51:22 -0400 Subject: [PATCH 46/54] . --- Yafc.I18n.Generator/SourceGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Yafc.I18n.Generator/SourceGenerator.cs b/Yafc.I18n.Generator/SourceGenerator.cs index 17e542ec..80b5f4cf 100644 --- a/Yafc.I18n.Generator/SourceGenerator.cs +++ b/Yafc.I18n.Generator/SourceGenerator.cs @@ -8,7 +8,7 @@ internal partial class SourceGenerator { private static void Main() { // Find the solution root directory string rootDirectory = Environment.CurrentDirectory; - while (!Directory.Exists(Path.Combine(rootDirectory, ".git"))) { + while (!Directory.Exists(Path.Combine(rootDirectory, "FactorioCalc.sln"))) { rootDirectory = Path.GetDirectoryName(rootDirectory)!; } Environment.CurrentDirectory = rootDirectory; From 0445c7b0bf02af95fd0c1449fedc00c512ef2f27 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 15:59:03 -0400 Subject: [PATCH 47/54] . --- Yafc.I18n.Generator/SourceGenerator.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Yafc.I18n.Generator/SourceGenerator.cs b/Yafc.I18n.Generator/SourceGenerator.cs index 80b5f4cf..ad7d2eac 100644 --- a/Yafc.I18n.Generator/SourceGenerator.cs +++ b/Yafc.I18n.Generator/SourceGenerator.cs @@ -8,11 +8,13 @@ internal partial class SourceGenerator { private static void Main() { // Find the solution root directory string rootDirectory = Environment.CurrentDirectory; - while (!Directory.Exists(Path.Combine(rootDirectory, "FactorioCalc.sln"))) { + while (!Directory.Exists(Path.Combine(rootDirectory, ".git"))) { rootDirectory = Path.GetDirectoryName(rootDirectory)!; } Environment.CurrentDirectory = rootDirectory; + Console.WriteLine("Found root directory: " + rootDirectory); + HashSet keys = []; HashSet referencedKeys = []; @@ -146,6 +148,8 @@ public string L({{string.Join(", ", Enumerable.Range(1, parameterCount).Select(n strings.WriteLine("}"); } + Console.WriteLine($"Loaded {keys.Count} strings."); + ReplaceIfChanged("Yafc.I18n/LocalizableStringClasses.g.cs", classesMemory); ReplaceIfChanged("Yafc.I18n/LocalizableStrings.g.cs", stringsMemory); } @@ -155,6 +159,10 @@ private static void ReplaceIfChanged(string filePath, MemoryStream newContent) { newContent.Position = 0; if (!File.Exists(filePath) || File.ReadAllText(filePath) != new StreamReader(newContent, leaveOpen: true).ReadToEnd()) { File.WriteAllBytes(filePath, newContent.ToArray()); + Console.WriteLine($"Updated {Path.GetFullPath(filePath)}."); + } + else { + Console.WriteLine($"{Path.GetFullPath(filePath)} is up-to-date."); } } From 04137314900e066a7bd5ef663f9ca163d729fc67 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 16:20:57 -0400 Subject: [PATCH 48/54] . --- .github/workflows/buildcheck-debug.yml | 5 +++++ .github/workflows/buildcheck-release.yml | 5 +++++ build.sh | 3 +++ 3 files changed, 13 insertions(+) diff --git a/.github/workflows/buildcheck-debug.yml b/.github/workflows/buildcheck-debug.yml index eae17bde..d7593401 100644 --- a/.github/workflows/buildcheck-debug.yml +++ b/.github/workflows/buildcheck-debug.yml @@ -35,6 +35,11 @@ jobs: - name: Setup MSBuild.exe uses: microsoft/setup-msbuild@v2 + # For reasons that are unclear, the build fails if the generated files do not exist at the beginning of the build. + # Explicitly build the generator to generate the files, then let the implicit build take care of everything else. + - name: Generate source + run: dotnet build ./Yafc.I18n.Generator/ + # Execute all unit tests in the solution - name: Execute unit tests run: dotnet test diff --git a/.github/workflows/buildcheck-release.yml b/.github/workflows/buildcheck-release.yml index d556704c..6a04df9d 100644 --- a/.github/workflows/buildcheck-release.yml +++ b/.github/workflows/buildcheck-release.yml @@ -34,6 +34,11 @@ jobs: - name: Setup MSBuild.exe uses: microsoft/setup-msbuild@v2 + # For reasons that are unclear, the build fails if the generated files do not exist at the beginning of the build. + # Explicitly build the generator to generate the files, then let the implicit build take care of everything else. + - name: Generate source + run: dotnet build ./Yafc.I18n.Generator/ + # Execute all unit tests in the solution - name: Execute unit tests run: dotnet test diff --git a/build.sh b/build.sh index a030c274..86c62a53 100755 --- a/build.sh +++ b/build.sh @@ -8,6 +8,9 @@ rm -rf Build VERSION=$(grep -oPm1 "(?<=)[^<]+" Yafc/Yafc.csproj) echo "Building YAFC version $VERSION..." +# For reasons that are unclear, the build fails if the generated files do not exist at the beginning of the build. +# Explicitly build the generator to generate the files, then let the implicit builds take care of everything else. +dotnet build ./Yafc.I18n.Generator/ dotnet publish Yafc/Yafc.csproj -r win-x64 -c Release -o Build/Windows dotnet publish Yafc/Yafc.csproj -r win-x64 --self-contained -c Release -o Build/Windows-self-contained dotnet publish Yafc/Yafc.csproj -r osx-x64 -c Release -o Build/OSX From 9c624eb9c01c01b3890965613721d47341b12b65 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Mon, 5 May 2025 16:28:24 -0400 Subject: [PATCH 49/54] . --- crowdin.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crowdin.yml b/crowdin.yml index 6940499a..9dc78157 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -13,8 +13,8 @@ "files": [ { - "source": "Yafc/Data/locale/en/yafc.cfg", - "dest": "yafc.ini", - "translation": "Yafc/Data/locale/%two_letters_code%/yafc.cfg" + "source": "Yafc/Data/locale/en/*.cfg", + "dest": "%file_name%.ini", + "translation": "Yafc/Data/locale/%two_letters_code%/%file_name%.cfg" } ] From 3e84d6beea669c6ac73e17f4e38a9e897ce6be6c Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 9 May 2025 19:16:34 -0400 Subject: [PATCH 50/54] . From 369d5bce5a22c907abc3c2fe1c47e281b52dbca0 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 9 May 2025 19:18:57 -0400 Subject: [PATCH 51/54] . From 2df05ee8251601b159f09a7cff4616ded56f1db6 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 9 May 2025 19:20:01 -0400 Subject: [PATCH 52/54] . From fc6abd50a2c997065cbc95a69cddce7cb9877f04 Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Fri, 9 May 2025 19:24:41 -0400 Subject: [PATCH 53/54] . From 8e3181f6fab97c5bff1fab0fffe360bbb03749ec Mon Sep 17 00:00:00 2001 From: Dale McCoy <21223975+DaleStan@users.noreply.github.com> Date: Sat, 10 May 2025 17:22:29 -0400 Subject: [PATCH 54/54] . --- Yafc/Data/locale/en/yafc.cfg | 850 +---------------------------------- 1 file changed, 2 insertions(+), 848 deletions(-) diff --git a/Yafc/Data/locale/en/yafc.cfg b/Yafc/Data/locale/en/yafc.cfg index a60d5cbc..8f90cd72 100644 --- a/Yafc/Data/locale/en/yafc.cfg +++ b/Yafc/Data/locale/en/yafc.cfg @@ -1,850 +1,4 @@ [yafc] ; Program.cs -yafc-with-version=YAFC CE v__1__ - -; FactorioDataSource.cs -progress-initializing=Initializing -progress-loading-mod-list=Loading mod list -could-not-read-mod-list=Could not read mod list from __1__ -mod-not-found-try-in-factorio=Mod not found: __1__. Try loading this pack in Factorio first. -progress-creating-lua-context=Creating Lua context -circular-mod-dependencies=Mods dependencies are circular. Unable to load mods: __1__ -; In most languages, this should have a trailing space: -list-separator=, -progress-completed=Completed! - -; LuaContext.cs -progress-executing-mod-at-data-stage=Executing mods __1__ - -; CommandLineParser.cs -missing-command-line-argument=Missing argument for __1__. -console-help-message=Usage:\nYafc [ [--mods-path ] [--project-file ] [--help]\n\nDescription:\n Yafc can be started without any arguments. However, if arguments are supplied, it is\n mandatory that the first argument is the path to the data directory of Factorio. The\n other arguments are optional in any case.\n\nOptions:\n \n Path of the data directory (mandatory if other arguments are supplied)\n\n --mods-path \n Path of the mods directory (optional)\n\n --project-file \n Path of the project file (optional)\n\n --help\n Display this help message and exit\n\nExamples:\n 1. Starting Yafc without any arguments:\n $ ./Yafc\n This opens the welcome screen.\n\n 2. Starting Yafc with a project path:\n $ ./Yafc path/to/my/project.yafc\n Skips the welcome screen and loads the project. If the project has not been\n opened before, then uses the start-settings of the most-recently-opened project.\n\n 3. Starting Yafc with the path to the data directory of Factorio:\n $ ./Yafc Factorio/data\n This opens a fresh project and loads the game data from the supplied directory.\n Fails if the directory does not exist.\n\n 4. Starting Yafc with the paths to the data directory and a project file:\n $ ./Yafc Factorio/data --project-file my-project.yafc\n This opens the supplied project and loads the game data from the supplied data\n directory. Fails if the directory and/or the project file do not exist.\n\n 5. Starting Yafc with the paths to the data & mods directories and a project file:\n $ ./Yafc Factorio/data --mods-path Factorio/mods --project-file my-project.yafc\n This opens the supplied project and loads the game data and mods from the supplied\n data and mods directories. Fails if any of the directories and/or the project file\n do not exist. -command-line-error-path-does-not-exist=__1__ path '__2__' does not exist. -folder-type-data=Data -folder-type-mods=Mods -folder-type-project=Project -command-line-error-unknown-argument=Unknown argument '__1__'. -command-line-error=Error: __1__ - -; ImmediateWidgets.cs -factorio-object-none=None -see-full-list-button=See full list -clear-button=Clear -per-second-suffix=/s -per-minute-suffix=/m -per-hour-suffix=/h -seconds-per-stack=__1__ per stack -select-quality=Select quality - -; MainScreenTabBar.cs -edit-page-properties=Edit properties -open-secondary-page=Open as secondary -shortcut-ctrl-click=Ctrl+Click -close-secondary-page=Close secondary -duplicate-page=Duplicate page - -; ObjectTooltip.cs -name-with-type=__1__ (__2__) -tooltip-nothing-to-list=Nothing -tooltip-and-more-in-list=... and __1__ more -tooltip-not-accessible=This __1__ is inaccessible, or it is only accessible through mod or map script. Middle-click to open dependency analyzer to investigate. -tooltip-not-automatable=This __1__ cannot be fully automated. This means that it requires either manual crafting, or manual labor such as cutting trees. -tooltip-not-automatable-yet=This __1__ cannot be fully automated at current milestones. -tooltip-has-untranslated-special-type=Special: __1__ -energy-electricity=Power usage: __1__ -energy-heat=Heat energy usage: __1__ -energy-labor=Labor energy usage: __1__ -energy-free=Free energy usage: __1__ -energy-fluid-fuel=Fluid fuel energy usage: __1__ -energy-fluid-heat=Fluid heat energy usage: __1__ -energy-solid-fuel=Solid fuel energy usage: __1__ -tooltip-header-loot=Loot -map-generation-density=Generates on map (estimated density: __1__) -map-generation-density-unknown=Generates on map (density unknown) -entity-crafts=Crafts -entity-crafting-speed=Crafting speed: __1__ -entity-crafting-productivity=Crafting productivity: __1__ -entity-energy-consumption=Energy consumption: __1__ -entity-module-slots=Module slots: __1__ -; __1__ is the list, and __2__ is the length of the list -allowed-module-effects-untranslated-list=Only allowed effect__plural_for_parameter__2__{1=|rest=s}__: __1__ -lab-allowed-inputs=Allowed input__plural_for_parameter__1__{1=|rest=s}__: -perishable=Perishable -tooltip-active-plus-drain-power=__1__ + __2__ -entity-absorbs-pollution=This building absorbs __1__ -entity-has-high-pollution=This building contributes to global warning! -belt-throughput=Belt throughput (Items): __1__ -inserter-swing-time=Swing time: __1__ -beacon-efficiency=Beacon efficiency: __1__ -accumulator-capacity=Accumulator charge: __1__ -lightning-attractor-extra-info=Power production (average usable): __1__\n Build in a __2__-tile square grid\nProtection range: __3__\nCollection efficiency: __4__ -solar-panel-average-production=Power production (average): __1__ -open-neie-middle-click-hint=Middle mouse button to open __YAFC__neie__ for this __1__ -tooltip-header-miscellaneous-sources=Source__plural_for_parameter__1__{1=|rest=s}__ -; English doesn't care about number of items in these lists, but we pass the count for the benefit of languages that might: -tooltip-header-production-recipes=Made with__plural_for_parameter__1__{rest=}__ -tooltip-header-consumption-recipes=Needed for__plural_for_parameter__1__{rest=}__ -; Always only one entity in this list: -tooltip-header-item-placement-result=Place result -tooltip-header-module-properties=Module parameters -productivity-property=Productivity: __1__ -speed-property=Speed: __1__ -consumption-property=Consumption: __1__ -pollution-property=Pollution: __1__ -quality-property=Quality: __1__ -item-stack-size=Stack size: __1__ -item-rocket-capacity=Rocket capacity: __1__ -analysis-better-recipes-to-create=YAFC analysis: There are better recipes to create __1__. (Wasting __2__% of YAFC cost) -analysis-better-recipes-to-create-all=YAFC analysis: There are better recipes to create each of the products. (Wasting __1__% of YAFC cost) -analysis-wastes-useful-products=YAFC analysis: This recipe wastes useful products. Don't do this recipe. -recipe-uses-fluid-temperature=Uses fluid temperature -recipe-uses-mining-productivity=Uses mining productivity -recipe-production-scales-with-power=Production scaled with power -; English doesn't care about number of items in several of these lists, but we pass the count for the benefit of languages that might: -tooltip-header-recipe-products=Product__plural_for_parameter__1__{1=|rest=s}__ -tooltip-header-recipe-crafters=Made in__plural_for_parameter__1__{rest=}__ -tooltip-header-allowed-modules=Allowed module__plural_for_parameter__1__{1=|rest=s}__ -tooltip-header-unlocked-by-technologies=Unlocked by__plural_for_parameter__1__{rest=}__ -technology-is-disabled=This technology is disabled and cannot be researched. -tooltip-header-technology-prerequisites=Prerequisite__plural_for_parameter__1__{1=|rest=s}__ -tooltip-header-technology-item-crafting=Item crafting required -tooltip-header-technology-capture=Capture __plural_for_parameter__1__{1=this|rest=any}__ entity -tooltip-header-technology-mine-entity=Mine __plural_for_parameter__1__{1=this|rest=any}__ entity -tooltip-header-technology-build-entity=Build __plural_for_parameter__1__{1=this|rest=any}__ entity -tooltip-header-technology-launch-item=Launch __plural_for_parameter__1__{1=this|rest=any}__ item -tooltip-header-unlocks-recipes=Unlocks recipe__plural_for_parameter__1__{1=|rest=s}__ -tooltip-header-unlocks-locations=Unlocks location__plural_for_parameter__1__{1=|rest=s}__ -tooltip-header-total-science-required=Total science required -tooltip-quality-upgrade-chance=Upgrade chance: __1__ (multiplied by module bonus) -tooltip-header-quality-bonuses=Quality bonuses -tooltip-no-normal-bonuses=Normal quality provides no bonuses. -tooltip-quality-crafting-speed=Crafting speed: -tooltip-quality-accumulator-capacity=Accumulator capacity: -tooltip-quality-module-effects=Module effects: -tooltip-quality-beacon-transmission=Beacon transmission efficiency: -tooltip-quality-time-before-spoiling=Time before spoiling: -tooltip-quality-lightning-attractor=Lightning attractor range & efficiency: -quality-bonus-value=+__1__ -quality-bonus-value-with-footnote=+__1__* -tooltip-quality-module-footnote=* Only applied to beneficial module effects. -product-suffix-preserved=, preserved until removed from the machine -tooltip-entity-spoils-after-no-production=After __1__ of no production, spoils into -tooltip-entity-expires-after-no-production=Expires after __1__ of no production -tooltip-entity-absorbs-pollution=Absorption: __1__ __2__ per minute -tooltip-entity-emits-pollution=Emission: __1__ __2__ per minute -tooltip-entity-requires-heat=Requires __1__ heat on cold planets. -tooltip-item-spoils=After __1__, spoils into - -; AboutScreen.cs -about-yafc=About YAFC-CE -full-name=Yet Another Factorio Calculator -about-community-edition=(Community Edition) -about-copyright-shadow=Copyright 2020-2021 ShadowTheAge -about-copyright-community=Copyright 2024 YAFC Community -about-copyleft-gpl-3=This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -about-warranty-disclaimer=This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -about-full-license-text=Full license text: -about-github-page=Github YAFC-CE page and documentation: -about-libraries=Free and open-source third-party libraries used: -about-dot-net-core=Microsoft .NET core and libraries -about-sdl2-libraries=Libraries for SDL2: -about-roboto-font-family=Roboto font family, -about-noto-sans-family=Noto Sans font family, -about-and=and -about-material-design-icon=Material Design Icon collection -about-plus=plus -about-serpent-library=Serpent library -about-and-small-bits=and small bits from -about-factorio-lua-api=Factorio API reference -about-factorio-trademark-disclaimer=Factorio name, content and materials are trademarks and copyrights of Wube Software. -about-factorio-wiki=Documentation on Factorio Wiki - -; DependencyExplorer.cs -dependency-fuel=__plural_for_parameter__1__{1=this|rest=of these}__ Fuel__plural_for_parameter__1__{1=|rest=s}__ -dependency-ingredient=__plural_for_parameter__1__{1=this|rest=of these}__ Ingredient__plural_for_parameter__1__{1=|rest=s}__ -dependency-crafter=__plural_for_parameter__1__{1=this|rest=of these}__ Crafter__plural_for_parameter__1__{1=|rest=s}__ -dependency-source=__plural_for_parameter__1__{1=this|rest=of these}__ Source__plural_for_parameter__1__{1=|rest=s}__ -dependency-technology=__plural_for_parameter__1__{1=this|rest=of these}__ Research__plural_for_parameter__1__{1=|rest=es}__ -dependency-item=Item____plural_for_parameter__1__{1=this|rest=of these}__ plural_for_parameter__1__{1=|rest=s}__ -dependency-location=__plural_for_parameter__1__{1=this|rest=of these}__ Location__plural_for_parameter__1__{1=|rest=s}__ -; __1__ here is the '__1__ is 1' form of one of the above strings: -dependency-require-single=Require __1__: -; __1__ here is the '__1__ is 2 or more' form of one of the above strings: -dependency-require-all=Require ALL __1__: -dependency-require-any=Require ANY __1__: - -; These messages should not end with a period. They get passed as __1__ to dependency-accessible-anyway or dependency-and-not-accessible -dependency-fuel-missing=There is no fuel to power this entity -dependency-ingredient-missing=There are no ingredients for this recipe -dependency-ingredient-variants-missing=There are no ingredient variants for this recipe -dependency-crafter-missing=There are no crafters that can craft this item -dependency-sources-missing=This item has no sources -dependency-technology-missing=This recipe is disabled and there are no technologies to unlock it -dependency-technology-no-prerequisites=There are no technology prerequisites -dependency-item-missing=This entity cannot be placed -dependency-map-source-missing=This recipe requires another entity -dependency-technology-disabled=This technology is disabled -dependency-location-missing=There are no locations that spawn this entity - -dependency-accessible-anyway=__1__, but it is inherently accessible. -dependency-and-not-accessible=__1__, and it is inaccessible. - -dependency-explorer=Dependency Explorer -dependency-currently-inspecting=Currently inspecting: -dependency-select-something=Select something -dependency-click-to-change-hint=(Click to change) -dependency-automatable=Status: Automatable -dependency-accessible=Status: Accessible, not automatable -dependency-marked-accessible=Manually marked as accessible -dependency-clear-mark=Clear mark -dependency-mark-not-accessible=Mark as inaccessible -dependency-mark-accessible-ignoring-milestones=Mark as accessible without milestones -dependency-marked-not-accessible=Status: Marked as inaccessible -dependency-not-accessible=Status: Not accessible. Wrong? -dependency-mark-accessible=Manually mark as accessible -dependency-header-dependencies=Dependenc__plural_for_parameter__1__{1=y|rest=ies}__: -dependency-header-dependents=Dependent__plural_for_parameter__1__{1=|rest=s}__: - -; ErrorListPanel.cs -error-loading-failed=Loading failed -error-but-loading-succeeded=Loading completed with errors -analysis-warnings=Analysis warnings - -; FilesystemScreen.cs -browser-create-directory=Create directory here - -; ImageSharePanel.cs -sharing-image-generated=Image generated -save-as-png=Save as PNG -save-and-open=Save to temp folder and open -copied-to-clipboard=Copied to clipboard -copy-to-clipboard-with-shortcut=Copy to clipboard (Ctrl+__1__) - -; MainScreen.cs -full-name-with-version=Yet Another Factorio Calculator CE v__1__ -create-production-sheet=Create production sheet (Ctrl+__1__) -list-and-search-all=List and search all pages (Ctrl+Shift+__1__) -menu-open-neie=Open NEIE -search-header=Find on page: -undo=Undo -shortcut-ctrl-X=Ctrl+__1__ -save=Save -save-as=Save As -find-on-page=Find on page -load-with-same-mods=Load another project (Same mods) -return-to-welcome-screen=Return to starting screen -menu-header-tools=Tools -menu-summary=Summary -menu-legacy-summary=Summary (Legacy) -menu-import-from-clipboard=Import page from clipboard -menu-header-extra=Extra -menu-run-factorio=Run Factorio -menu-check-for-updates=Check for updates -menu-about=About YAFC -alert-unsaved-changes=You have __1__ unsaved changes -alert-unsaved-changes-in-file=You have __1__ unsaved changes to __2__ -query-save-changes=Save unsaved changes? -dont-save=Don't save -new-version-available=New version available! -new-version-number=There is a new version available: __1__ -visit-release-page=Visit release page -close=Close -no-newer-version=No newer version -running-latest-version=You are running the latest version! -network-error=Network error -error-while-checking-for-new-version=There was an error while checking versions. -save-project-window-title=Save project -save-project-window-header=Save project as -load-project-window-title=Load project -load-project-window-header=Load another .yafc project -error-critical-loading-exception=Critical loading exception -default-file-name=project - -; MainScreen.PageListSearch.cs -search-all-header=Search in: -search-all-location-page-name=Page name -search-all-location-outputs=Desired products -search-all-location-recipes=Recipes -search-all-location-inputs=Ingredients -search-all-location-extra-outputs=Extra products -search-all-location-all=All -search-all-localized-strings=Localized names -search-all-internal-strings=Internal names -search-all-both-strings=Both -search-all-middle-mouse-to-edit-hint=Middle mouse button to edit - -; MilestonesEditor.cs -milestone-editor=Milestone editor -milestone-description=Hint: You can reorder milestones. When an object is locked behind a milestone, the first inaccessible milestone will be shown. Also when there is a choice between different milestones, first will be chosen. -milestone-auto-sort=Auto sort milestones -milestone-add=Add milestone -milestone-add-new=Add new milestone -milestone-cannot-add=Cannot add milestone -milestone-cannot-add-already-exists=Milestone already exists - -; MilestonesPanel.cs -milestones=Milestones -milestones-header=Please select objects that you already have access to: -milestones-description=For your convenience, YAFC will show objects you DON'T have access to based on this selection.\nThese are called 'Milestones'. By default all science packs and locations are added as milestones, but this does not have to be this way! You can define your own milestones: Any item, recipe, entity or technology may be added as a milestone. For example you can add advanced electronic circuits as a milestone, and YAFC will display everything that is locked behind those circuits -milestones-edit=Edit milestones -milestones-edit-settings=Edit tech progression settings -done=Done - -; NeverEnoughItemsPanel.cs -neie=Never Enough Items Explorer -neie-accepted-variants=Accepted fluid variants -neie-building-hours-suffix=__1__bh -neie-building-hours-description=Building-hours.\nAmount of building-hours required for all researches assuming crafting speed of 1. -fuel-value-can-be-used=Fuel value __1__ can be used for: -fuel-value-zero-can-be-used=Can be used to fuel: -neie-show-special-recipes=Show special recipes (barreling / voiding) -neie-show-locked-recipes=There are more recipes, but they are locked based on current milestones. -neie-show-inaccessible-recipes=There are more recipes, but they are inaccessible. -neie-show-more-recipes=Show more recipes -neie-special-recipes=Special recipes: -neie-locked-recipes=Locked recipes: -neie-inaccessible-recipes=Inaccessible recipes: -neie-hide-recipes=hide -neie-header=Never Enough Items Explorer -select-item=Select item -neie-production=Production: -neie-usage=Usages: -neie-colored-bars-link=What do colored bars mean? -neie-how-to-read-colored-bars=How to read colored bars -neie-colored-bars-description=Blue bar means estimated production or consumption of the thing you selected. Blue bar at 50% means that that recipe produces(consumes) 50% of the product.\n\nOrange bar means estimated recipe efficiency. If it is not full, the recipe looks inefficient to YAFC.\n\nIt is possible for a recipe to be efficient but not useful - for example a recipe that produces something that is not useful.\n\nYAFC only estimates things that are required for science recipes. So buildings, belts, weapons, fuel - are not shown in estimations. -neie-current-milestones-checkbox=Current milestones info - -; PreferencesScreen.cs -preferences-tab-general=General -preferences-tab-progression=Progression -preferences=Preferences -preferences-default-belt=Default belt: -preferences-default-inserter=Default inserter: -preferences-inserter-capacity=Inserter capacity: -preferences-target-technology=Target technology for cost analysis: -preferences-mining-productivity-bonus=Mining productivity bonus: -preferences-research-speed-bonus=Research speed bonus: -preferences-research-productivity-bonus=Research productivity bonus: -preferences-technology-level=__1__ Level: -prefs-unit-of-time=Unit of time: -prefs-unit-seconds=Second -prefs-unit-minutes=Minute -prefs-time-unit-hours=Hour -prefs-time-unit-custom=Custom -prefs-header-item-units=Item production/consumption: -prefs-header-fluid-units=Fluid production/consumption: -prefs-pollution-cost-hint=0% for off, 100% for old default -prefs-pollution-cost=Pollution cost modifier -prefs-icon-scale-hint=Some mod icons have little or no transparency, hiding the background color. This setting reduces the size of icons that could hide link information. -prefs-icon-scale=Display scale for linkable icons -prefs-milestones-per-line-hint=Some tooltips may want to show multiple rows of milestones. Increasing this number will draw fewer lines in some tooltips, by forcing the milestones to overlap.\n\nMinimum: 22\nDefault: 28 -prefs-milestones-per-line=Maximum milestones per line in tooltips: -prefs-reactor-layout=Reactor layout: -prefs-spoiling-rate-hint=Set this to match the spoiling rate you selected when starting your game. 10% is slow spoiling, and 1000% (1k%) is fast spoiling. -prefs-spoiling-rate=Spoiling rate: -prefs-show-inaccessible-milestone-overlays=Show milestone overlays on inaccessible objects -prefs-dark-mode=Dark mode -prefs-autosave=Enable autosave (Saves when the window loses focus) -prefs-goods-unit-simple=Simple Amount__1__ -prefs-goods-unit-custom=Custom: 1 unit equals -prefs-goods-unit-from-belt=Set from belt -prefs-select-belt=Select belt -per-second-suffix-long=per second -prefs-reactor-x-y-separator=x - -; ProjectPageSettingsPanel.cs -page-settings-name-hint=Input name -select-icon=Select icon -page-settings-icon-hint=And select icon -page-settings-create-header=Create new page -page-settings-edit-header=Edit page icon and name -create=Create -ok=OK -cancel=Cancel -page-settings-other=Other tools -delete-page=Delete page -export-page-to-clipboard=Share (export string to clipboard) -page-settings-screenshot=Make full page screenshot -page-settings-export-calculations=Export calculations (to clipboard) -alert-import-page-newer-version=Share string was created with a newer version of YAFC (__1__). Data may be lost. -alert-import-page-incompatible-version=Share string was created with a newer version of YAFC (__1__) and is incompatible. -alert-import-page-invalid-string=Clipboard text does not contain valid YAFC share string. -import-page-already-exists=Page already exists -import-page-already-exists-long=Looks like this page already exists with name '__1__'. Would you like to replace it or import as a copy? -replace=Replace -import-as-copy=Import as copy -export-no-fuel-selected=No fuel selected -export-recipe-disabled=Recipe disabled - -; SelectMultiObjectPanel.cs -select-multiple-objects-hint=Hint: Ctrl+click to select multiple - -; SelectObjectPanel.cs -type-for-search-hint=Start typing for search - -; ShoppingListScreen.cs -shopping-list-count-of-items=x__1__: __2__ -shopping-list-total-buildings=Total buildings -shopping-list-total-buildings-hint=Display the total number of buildings required, ignoring the built building count. -shopping-list-built-buildings=Built buildings -shopping-list-built-buildings-hint=Display the number of buildings that are reported in built building count. -shopping-list-missing-buildings=Missing buildings -shopping-list-missing-buildings-hint=Display the number of additional buildings that need to be built. -shopping-list-assume-no-buildings=No buildings -shopping-list-assume-no-buildings-hint=When the built building count is not specified, behave as if it was set to 0. -shopping-list-assume-enough-buildings=Enough buildings -shopping-list-assume-enough-buildings-hint=When the built building count is not specified, behave as if it matches the required building count. -shopping-list=Shopping list -shopping-list-cost-information=Total cost of all objects: ¥__1__, buildings: __2__, modules: __3__ -shopping-list-building-assumption-header=When not specified, assume: -shopping-list-allow-additional-heat=Allow additional heat for -shopping-list-heat-for-inserters=inserters -shopping-list-heat-for-pipes=pipes -shopping-list-heat-for-belts=belts -shopping-list-heat-and=, and -shopping-list-heat-for-other-entities=other entities -shopping-list-heat-period=. -shopping-list-decompose=Decompose -shopping-list-export-blueprint=Export to blueprint -shopping-list-export-blueprint-hint=Blueprint string will be copied to the clipboard. -shopping-list-these-require-heat=These entities require __1__ heat on cold planets. - -; WelcomeScreen.cs -welcome-project-file-location=Project file location -welcome-project-file-location-hint=You can leave it empty for a new project -welcome-data-location=Factorio Data location*\nIt should contain folders 'base' and 'core' -welcome-data-location-hint=e.g. C:/Games/Steam/SteamApps/common/Factorio/data -welcome-mod-location=Factorio Mods location (optional)\nIt should contain file 'mod-list.json' -welcome-mod-location-hint=If you don't use separate mod folder, leave it empty -; Possibly translate this as just "Language:"? -welcome-language-header=In-game objects language: -welcome-load-autosave=Load most recent (auto-)save -welcome-load-autosave-hint=When enabled it will try to find a more recent autosave. Disable if you want to load your manual save only. -welcome-use-net-production=Use net production/consumption when analyzing recipes -welcome-use-net-production-hint=If checked, YAFC will only suggest production or consumption recipes that have a net production or consumption of that item or fluid.\nFor example, kovarex enrichment will not be suggested when adding recipes that produce U-238 or consume U-235. -welcome-software-render=Force software rendering in project screen -welcome-software-render-hint=If checked, the main project screen will not use hardware-accelerated rendering.\n\nEnable this setting if YAFC crashes after loading without an error message, or if you know that your computer's graphics hardware does not support modern APIs (e.g. DirectX 12 on Windows). -recent-projects=Recent projects -toggle-dark-mode=Toggle dark mode -load-error-create-issue=If all else fails, then create an issue on GitHub -load-error-create-issue-with-information=Please attach a new-game save file to sync mods, versions, and settings. -welcome-alert-download-font=Yafc will download a suitable font before it restarts.\nThis may take a minute or two. -confirm=Confirm -welcome-alert-english-fallback=Mods may not support your language, using English as a fallback. -welcome-alert-need-a-different-font=These languages are not supported by the current font. Click the language to restart with a suitable font, or click '__YAFC__welcome-select-font__' to select a custom font. -welcome-select-font=Select font -welcome-reset-font=Reset font to default -welcome-reset-font-restart=Restart Yafc to switch to the selected font. -override-font=Override font -override-font-long=Override the font that YAFC uses -load-project-name=Load '__1__' -welcome-alert-missing-directory=Project directory does not exist -welcome-create-project-name=Create '__1__' -welcome-create-unnamed-project=Create new project -welcome-browse-button=... -select=Select -select-folder=Select folder -disable-and-reload=Disable & reload -disable-and-reload-hint=Disable this mod until you close YAFC or change the mod folder. -welcome=Welcome to YAFC CE v__1__ -; Translating these two may be uninteresting; they are only displayed when we cannot yet display the user's desired language: -please-wait=Please wait . . . -downloading-fonts=YAFC is downloading the fonts for your language. -unable-to-load-with-mod=YAFC was unable to load the project. You can disable the problematic mod once by clicking on the '__YAFC__disable-and-reload__' button, or you can disable it permanently for YAFC by copying the mod-folder, disabling the mod in the copy by editing mod-list.json, and pointing YAFC to the copy. -unable-to-load=YAFC cannot proceed because it was unable to load the project. -copy-to-clipboard=Copy to clipboard -error-while-loading-mod=Error while loading mod __1__. -more-info=More info -back-button=Back -load-error-advice=Check that these mods load in Factorio.\nYAFC only supports loading mods that were loaded in Factorio before. If you add or remove mods or change startup settings, you need to load those in Factorio and then close the game because Factorio saves mod-list.json only when exiting.\nCheck that Factorio loads mods from the same folder as YAFC.\nIf that doesn't help, try removing the mods that have several versions, or are disabled, or don't have the required dependencies. - -; WizardPanel.cs -wizard-finish=Finish -wizard-next=Next -wizard-previous=Previous -wizard-step-X-of-Y=Step __1__ of __2__ - -; AutoPlannerView.cs -auto-planner=Auto planner -auto-planner-warning=This is an experimental feature and may lack functionality. Unfortunately, after some prototyping it wasn't very useful to work with. More research required. -auto-planner-page-name=Enter page name: -auto-planner-goal=Select your goal: -auto-planner-select-production-goal=New production goal -auto-planner-review-milestones=Review active milestones, as they will restrict recipes that are considered: - -; SummaryView.cs -page=Page -summary-column-linked=Linked -summary-header=Production Sheet Summary -summary-only-show-issues=Only show issues -summary-auto-balance-hint=Attempt to match production and consumption of all linked products on the displayed pages.\n\nYou will often have to click this button multiple times to fully balance production. -summary-auto-balance=Auto balance - -; Analysis.cs -progress-running-analysis=Running analysis algorithms - -; CostAnalysis.cs -cost-analysis-estimated-amount-for=Estimated amount for __1__: -cost-analysis-estimated-amount=Estimated amount for all researches: -cost-analysis-failed=Cost analysis was unable to process this modpack. This may indicate a bug in Yafc. -analysis-not-automatable=YAFC analysis: Unable to find a way to fully automate this. -cost-analysis-fluid-cost=YAFC cost per 50 units of fluid: ¥__1__ -cost-analysis-item-cost=YAFC cost per item: ¥__1__ -cost-analysis-energy-cost=YAFC cost per 1 MW: ¥__1__ -cost-analysis-recipe-cost=YAFC cost per recipe: ¥__1__ -cost-analysis-generic-cost=YAFC cost: ¥__1__ -; __1__ is one of the above cost-analysis-*-cost strings -cost-analysis-with-current-cost=__1__ (Currently ¥__2__) - -; DependencyNode.cs -dependency-or-bar=-- OR -- - -; DataClasses.cs -ingredient-amount=__1__x __2__ -ingredient-amount-with-temperature=__1__x __2__ (__3__) -product-amount=__1__x __2__ -product-amount-range=__1__-__2__x __3__ -product-probability=__1__ __2__ -product-probability-amount=__1__ __2__x __3__ -product-probability-amount-range=__1__ __2__-__3__x __4__ -; __1__ is one of the above product strings -product-always-fresh=__1__, always fresh -product-fixed-spoilage=__1__, __2__ spoiled - -temperature=__1__° -temperature-range=__1__°-__2__° - -; DataUtils.cs -ctrl-click-hint-complete-milestones=Hint: Complete milestones to enable ctrl+click -ctrl-click-hint-mark-accessible=Hint: Mark a recipe as accessible to enable ctrl+click -ctrl-click-hint-will-add-favorite=Hint: Ctrl+click to add your favorited recipe -ctrl-click-hint-multiple-favorites=Hint: Cannot ctrl+click with multiple favorited recipes -ctrl-click-hint-will-add-normal=Hint: Ctrl+click to add the accessible normal recipe -ctrl-click-hint-will-add-special=Hint: Ctrl+click to add the accessible recipe -ctrl-click-hint-set-favorite=Hint: Set a favorite recipe to add it with ctrl+click -format-time-in-seconds=__1__ seconds -format-time-in-minutes=__1__ minutes -format-time-in-hours=__1__ hours - -; AutoPlanner.cs -auto-planner-missing-goal=Auto planner goal no longer exists. -auto-planner-no-solution=Model has no solution - -; ProductionSummary.cs -legacy-summary-page-missing=Page missing -legacy-summary-broken-entry=Broken entry -load-error-referenced-page-not-found=Referenced page does not exist -load-error-object-does-not-exist=Object does not exist - -; ProductionTable.cs -production-table-no-solution-and-no-deadlocks=YAFC failed to solve the model and to find deadlock loops. As a result, the model was not updated. -production-table-numerical-errors=This model has numerical errors (probably too small or too large numbers) and cannot be solved. -production-table-unexpected-error=Unaccounted error: MODEL___1__ -production-table-requires-more-buildings=This model requires more buildings than are currently built. - -; ProductionTableContent.cs -link-warning-no-production=This link has no production. (Link ignored) -link-warning-no-consumption=This link has no consumption. (Link ignored) -link-warning-unmatched-nested-link=Nested table link has unmatched production/consumption. These unmatched products are not captured by this link. -link-message-remove-to-link-with-parent=Nested tables have their own set of links that DON'T connect to parent links. To connect this product to the outside, remove this link. -link-warning-negative-feedback=YAFC was unable to satisfy this link (Negative feedback loop). This doesn't mean that this link is the problem, but it is part of the loop. -link-warning-needs-overproduction=YAFC was unable to satisfy this link (Overproduction). You can allow overproduction for this link to solve the error. -load-error-recipe-does-not-exist=Recipe does not exist. -load-error-linked-product-does-not-exist=Linked product does not exist. - -; Project.cs -error-loading-autosave=Fatal error reading the latest autosave. Loading the base file instead. -load-error-did-not-read-all-data=Json was not consumed to the end! -load-warning-newer-version=This file was created with future YAFC version. This may lose data. -load-error-unable-to-load-file=Unable to load the project file. - -; ErrorCollector.cs -repeated-error=__1__ (x__2__) - -; PropertySerializers.cs -load-warning-unexpected-object=Project contained an unexpected object. - -; SerializationMap.cs -load-error-unable-to-deserialize-untranslated=Unable to deserialize __1__. -load-error-encountered-unexpected-value=Encountered an unexpected value when reading the project file. - -; ValueSerializers.cs -load-error-untranslated-type-does-not-exist=Type __1__ does not exist. Possible plugin version change. -load-error-fluid-has-incorrect-temperature=Fluid __1__ doesn't have correct temperature information. May require adjusting its temperature. -load-error-untranslated-factorio-object-not-found=Factorio object '__1__' no longer exists. Check mods configuration. -load-error-untranslated-factorio-quality-not-found=Factorio quality '__1__' no longer exists. Check mods configuration. - -; FactorioDataDeserializer.cs -progress-loading=Loading -progress-loading-items=Loading items -progress-loading-tiles=Loading tiles -progress-loading-fluids=Loading fluids -progress-loading-recipes=Loading recipes -progress-loading-locations=Loading locations -progress-loading-technologies=Loading technologies -progress-loading-qualities=Loading qualities -progress-loading-entities=Loading entities -progress-postprocessing=Post-processing -progress-computing-maps=Computing maps -progress-calculating-dependencies=Calculating dependencies -progress-creating-project=Creating project -progress-rendering-icons=Rendering icons -progress-rendering-X-of-Y=__1__/__2__ -progress-building-objects=Building objects - -special-recipe-launched=__1__ launched -special-recipe-generating=__1__ generating -special-recipe-launch=__1__ launch -special-recipe-boiling=__1__ boiling to __2__° -special-recipe-pumping=__1__ pumping -special-recipe-mining=__1__ mining -special-recipe-planting=__1__ planting -special-recipe-spoiling=__1__ spoiling - -; FactorioDataDeserializer_Context.cs -special-object-electricity=Electricity -special-object-electricity-description=This is an object that represents electric energy -special-object-heat=Heat -special-object-heat-description=This is an object that represents heat energy -special-object-void=Void -special-object-void-description=This is an object that represents infinite energy -special-object-launch-slot=Rocket launch slot -special-object-launch-slot-description=This is a slot in a rocket ready to be launched -special-item-total-consumption=Total item consumption -special-item-total-consumption-description=This item represents the combined total item input of a multi-ingredient recipe. It can be used to set or measure the number of sushi belts required to supply this recipe row. -special-item-total-production=Total item production -special-item-total-production-description=This item represents the combined total item output of a multi-product recipe. It can be used to set or measure the number of sushi belts required to handle the products of this recipe row. -localization-fallback-description-recipe-to-create=A recipe to create __1__ -localization-fallback-description-item-to-build=An item to build __1__ -fluid-name-with-temperature=__1__ __2__° -fluid-description-temperature-solo=Temperature: __1__° -fluid-description-temperature-added=Temperature: __1__°\n__2__ - -; FactorioDataDeserializer_RecipeAndTechnology.cs -research-has-an-unsupported-trigger-type=Research trigger of __1__ has an unsupported type __2__ - -; Milestones.cs -milestone-analysis-most-inaccessible=More than 50% of all in-game objects appear to be inaccessible in this project with your current mod list. This can have a variety of reasons like objects being accessible via scripts__YAFC__milestone-analysis-or-bug__ __YAFC__milestone-analysis-is-important__ -milestone-analysis-no-rocket-launch=Rocket launch appears to be inaccessible. This means that rocket may not be launched in this mod pack, or it requires mod script to spawn or unlock some items__YAFC__milestone-analysis-or-bug__ __YAFC__milestone-analysis-is-important__ -milestone-analysis-inaccessible-milestones=There are some milestones that are not accessible: __1__. You may remove these from milestone list__YAFC__milestone-analysis-or-bug__ __YAFC__milestone-analysis-is-important__ -milestone-analysis-or-bug=, or it might be due to a bug inside a mod or YAFC. -milestone-analysis-is-important=A lot of YAFC's systems rely on objects being accessible, so some features may not work as intended.\n\nFor this reason YAFC has a Dependency Explorer that allows you to manually enable some of the core recipes. YAFC will iteratively try to unlock all the dependencies after each recipe you manually enabled. For most modpacks it's enough to unlock a few early recipes like any special recipes for plates that everything in the mod is based on. - -; ImGuiUtils.cs -search-hint=Search - -; ProductionSummaryView.cs -legacy-summary-group-name-hint=Group name -legacy-summary-go-to-page=Go to page -remove=Remove -legacy-summary-other-column=Other -legacy-summary-empty-group=This is an empty group -legacy-summary-empty-group-description=Add your existing sheets here to keep track of what you have in your base and to see what shortages you may have. -legacy-summary-group-description=List of goods produced/consumed by added blocks. Click on any of these to add it to (or remove it from) the table. -legacy-summary-multiplier-edit-box-prefix=x - -; ModuleCustomizationScreen.cs -module-customization=Module customization -module-customization-name-hint=Enter name -module-customization-filter-buildings=Filter by crafting buildings (Optional): -module-customization-add-filter-building=Add module template filter -module-customization-enable=Enable custom modules -module-customization-internal-modules=Internal modules: -module-customization-leave-zero-hint=Specify zero modules to fill all remaining slots. -module-customization-beacons-only=This building doesn't have module slots, but can be affected by beacons. -module-customization-beacon-modules=Beacon modules: -module-customization-using-default-beacons=Use default parameters -module-customization-override-beacons=Override beacons as well -module-customization-use-number-of-modules-in-beacons=Enter the number of modules, not the number of beacons. A single beacon can hold __1__ module__plural_for_parameter__1__{1=|rest=s}__. -module-customization-current-effects=Current effects: -module-customization-productivity-bonus=Productivity bonus: __1__ -module-customization-speed-bonus=Speed bonus: __1__ (Crafting speed: __2__) -module-customization-quality-bonus=Quality bonus: __1__ (multiplied by quality upgrade chance) -module-customization-energy-usage=Energy usage: __1__ -module-customization-overall-speed=Overall crafting speed (including productivity): __1__ -module-customization-energy-cost-per-output=Energy cost per recipe output: __1__ -module-customization-energy-usage-per-building=Energy usage: __1__ (__2__ per building) -partial-cancel=Cancel (partial) -module-customization-remove=Remove module customization -select-beacon=Select beacon -select-module=Select module - -; ModuleFillerParametersScreen.cs -affected-by-M-beacons=Affected by __1__ -each-containing-N-modules=each containing __1__ -module-filler-remove-current-override-hint=Click here to remove the current override. -select-beacon-module=Select beacon module -use-no-modules=Use no modules -use-best-modules=Use best modules -module-filler-payback-estimate=Modules payback estimate: __1__ -module-filler-header-autofill=Module autofill parameters -module-filler-fill-miners=Fill modules in miners -module-filler-module=Filler module: -module-filler-module-hint=Use this module when autofill doesn't add anything (for example when productivity modules don't fit) -module-filler-select-module=Select filler module -module-filler-header-beacons=Beacons & beacon modules: -module-filler-no-beacons=Your mods contain no beacons, or no modules that can be put into beacons. -module-filler-select-beacon-module=Select module for beacon -module-filler-beacons-per-building=Beacons per building: -module-filler-beacons-not-calculated=Please note that beacons themselves are not part of the calculation. -module-filler-override-beacons=Override beacons: -module-filler-override-beacons-hint=Click to change beacon, right-click to change module\nSelect the 'none' item in either prompt to remove the override. -module-filler-add-beacon-override=Add an override for a building type -module-filler-select-overridden-crafter=Add exception(s) for: - -; ModuleTemplateConfiguration.cs -module-templates=Module templates -create-new-template-hint=Create new template - -; ProductionLinkSummaryScreen.cs -link-summary-production=Production: __1__ -link-summary-implicit-links=Plus additional production from implicit links -link-summary-consumption=Consumption: __1__ -link-summary-child-links=Child links: -link-summary-parent-links=Parent links: -link-summary-unrelated-links=Unrelated links: -link-summary-unlinked=Unlinked: -link-summary=Link summary -link-summary-header=Exploring link for: -remove-link=Remove link -link-summary-no-products=This recipe has no linked products. -link-summary-no-ingredients=This recipe has no linked ingredients. -link-summary-select-product=Select product link to inspect -link-summary-select-ingredient=Select ingredient link to inspect -link-summary-requested-production=Requested production: __1__ -link-summary-requested-consumption=Requested consumption: __1__ -link-summary-overproduction=Overproduction: __1__ -link-summary-overconsumption=overconsumption: __1__ -link-summary-link-nested-under=This link is nested under: -link-summary-recipe-nested-under=This recipe is nested under: - -; ProductionTableView.cs -production-table-header-recipe=Recipe -production-table-header-entity=Entity -production-table-header-ingredients=Ingredients -production-table-header-products=Products -production-table-header-modules=Modules -production-table-add-raw-recipe=Add raw recipe -production-table-add-technology=Add technology -production-table-export-to-blueprint=Export inputs and outputs to blueprint with constant combinators: -export-blueprint-amount-per=Amount per: -export-blueprint-amount-per-second=second -export-blueprint-amount-per-minute=minute -export-blueprint-amount-per-hour=hour -production-table-remove-zero-building-recipes=Remove all zero-building recipes -production-table-add-technology-hint=Ctrl+click to add a technology instead -production-table-clear-recipes=Clear recipes -production-table-add-all-recipes=Add ALL recipes -production-table-mass-set-assembler=Set assembler for all recipes -production-table-mass-set-quality=Set quality for all recipes -production-table-mass-set-fuel=Set fuel for all recipes -production-table-auto-modules=Auto modules -production-table-module-settings=Module settings -production-table-desired-products=Desired products and amounts (Use negative for input goal): -production-table-add-desired-product=Add desired product -production-table-summary-ingredients=Summary ingredients: -production-table-import-ingredients=Import ingredients: -production-table-extra-products=Extra products: -production-table-export-products=Export products: -production-table-nested-group=This is a nested group. You can drag&drop recipes here. Nested groups can have their own linked materials. - -production-table-create-nested=Create nested table -production-table-add-nested-product=Add nested desired product -production-table-unpack-nested=Unpack nested table -production-table-shortcut-right-click=Shortcut: Right-click -production-table-shortcut-expand-and-right-click=Shortcut: Expand, then right-click -production-table-show-total-io=Show total Input/Output -enabled=Enabled -add-recipe-to-favorites=Add recipe to favorites -favorite=Favorite -production-table-delete-nested=Delete nested table -production-table-shortcut-collapse-and-right-click=Shortcut: Collapse, then right-click -production-table-delete-recipe=Delete recipe - -production-table-select-crafting-entity=Select crafting entity -production-table-select-accumulator=Select accumulator -production-table-clear-fixed-multiplier=Clear fixed recipe multiplier -production-table-set-fixed-building-count=Set fixed building count -production-table-clear-fixed-building-count=Clear fixed building count -production-table-fixed-buildings-hint=Tell YAFC how many buildings it must use when solving this page.\nUse this to ask questions like 'What does it take to handle the output of ten miners?' -production-table-set-built-building-count=Set built building count -production-table-clear-built-building-count=Clear built building count -production-table-built-building-count-hint=Tell YAFC how many of these buildings you have in your factory.\nYAFC will warn you if you need to build more buildings. -production-table-generate-building-blueprint=Create single building blueprint -production-table-generate-building-blueprint-hint=Generate a blueprint for one of these buildings, with the recipe and internal modules set. -production-table-add-building-to-favorites=Add building to favorites - -production-table-alert-no-known-fuels=This entity has no known fuels -production-table-add-fuel-to-favorites=Add fuel to favorites -production-table-select-fuel=Select fuel -production-table-accepted-fluids=Accepted fluid variants: -production-table-add-production-recipe=Add production recipe -production-table-create-table-for=Create new production table for __1__ -production-table-produce-as-spent-fuel=Produce as spent fuel -production-table-add-consumption-recipe=Add consumption recipe -production-table-add-fuel-usage=Add fuel usage -production-table-add-consumption-technology=Add consumption technology -production-table-add-multiple-hint=Hint: Ctrl+click to add multiple -production-table-allow-overproduction=Allow overproduction -production-table-view-link-summary=View link summary -production-table-cannot-unlink=__1__ is a desired product and cannot be unlinked. -production-table-currently-linked=__1__ production is currently linked. This means that YAFC will try to match production with consumption. -production-table-remove-and-unlink-desired-product=Remove and unlink -production-table-remove-desired-product=Remove desired product -production-table-unlink=Unlink -production-table-linked-in-parent=__1__ production is currently linked, but the link is outside this nested table. Nested tables can have their own separate links. -production-table-implicitly-linked=__1__ (__2__) production is implicitly linked. This means that YAFC will use it, along with all other available qualities, to produce __ITEM__science__.\nYou may add a regular link to replace this implicit link. -production-table-create-link=Create link -production-table-not-linked=__1__ production is currently NOT linked. This means that YAFC will make no attempt to match production with consumption. - -production-table-set-fixed-fuel=Set fixed fuel consumption -production-table-set-fixed-ingredient=Set fixed ingredient consumption -production-table-set-fixed-product=Set fixed production amount -production-table-set-fixed-will-replace=This will replace the other fixed amount in this row. -production-table-clear-fixed-fuel=Clear fixed fuel consumption -production-table-clear-fixed-ingredient=Clear fixed ingredient consumption -production-table-clear-fixed-product=Clear fixed production amount -production-table-buildings-per-half-belt=(Buildings per half belt: __1__) -production-table-inserters-per-building=__1__ (__2__/building) -production-table-approximate-inserters=~__1__ -production-table-approximate-inserters-per-building=~__1__ (__2__/b) - -production-table-output-preserved-in-machine=This recipe output does not start spoiling until removed from the machine. -production-table-output-always-fresh=This recipe output is always fresh. -production-table-output-fixed-spoilage=This recipe output is __1__ spoiled. - -production-table-module-template-incompatible=This module template seems incompatible with the recipe or the building. -production-table-use-default-modules=Use default modules -production-table-select-modules=Select fixed module -production-table-use-module-template=Use module template: -production-table-configure-module-templates=Configure module templates -production-table-customize-modules=Customize modules - -production-table-alert-recipe-exists=Recipe already exists -production-table-query-add-copy=Add a second copy of __1__? -production-table-add-copy=Add a copy - -warning-description-deadlock-candidate=Contains recursive links that cannot be matched. No solution exists. -warning-description-overproduction-required=This model cannot be solved exactly, it requires some overproduction. You can allow overproduction for any link. This recipe contains one of the possible candidates. -warning-description-entity-not-specified=Crafter not specified. Solution is inaccurate. -warning-description-fuel-not-specified=Fuel not specified. Solution is inaccurate. -warning-description-fluid-with-temperature=This recipe uses fuel with temperature. Should link with producing entity to determine temperature. -warning-description-fluid-too-hot=Fluid temperature is higher than generator maximum. Some energy is wasted. -warning-description-fuel-does-not-provide-energy=This fuel cannot provide any energy to this building. The building won't work. -warning-description-has-max-fuel-consumption=This building has max fuel consumption. The rate at which it works is limited by it. -warning-description-ingredient-temperature-range=This recipe cares about ingredient temperature, and the temperature range does not match. -warning-description-assumes-reactor-formation=Assumes reactor formation from preferences. __YAFC__warning-description-click-for-preferences__ -warning-description-assumes-nauvis-solar=Energy production values assume Nauvis solar ratio (70% power output). Don't forget accumulators. -warning-description-needs-more-buildings=This recipe requires more buildings than are currently built. -warning-description-asteroid-collectors=The speed of asteroid collectors depends heavily on location and travel speed. It also depends on the distance between adjacent collectors. These dependencies are not modeled. Expect widely varied performance. -warning-description-assumes-fulgoran-lightning=Energy production values assume Fulgoran storms and attractors in a square grid.\nThe accumulator estimate tries to store 10% of the energy captured by the attractors. -warning-description-useless-quality=The quality bonus on this recipe has no effect. Make sure the recipe produces items and that all milestones for the next quality are unlocked. __YAFC__warning-description-click-for-milestones__ -warning-description-excess-productivity-bonus=This building has a larger productivity bonus (from base effect, research, and/or modules) than allowed by the recipe. Please make sure you entered productivity research levels, not percent bonuses. __YAFC__warning-description-click-for-preferences__ -warning-description-click-for-preferences=(Click to open the preferences) -warning-description-click-for-milestones=(Click to open the milestones window) - -; ProjectPage.cs -default-new-page-name=New page - -; ExceptionScreen.cs -exception-ignore-future-errors=Ignore future errors +N-items=__1__ item__plural_for_parameter__1__{1=|rest=s}__ +N-items-quoted="__1__ item__plural_for_parameter__1__{1=|rest=s}__"