Skip to content
1 change: 1 addition & 0 deletions Yafc.Model/Model/ProductionTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ public void AddRecipe(IObjectWithQuality<RecipeOrTechnology> recipe, IComparer<G
}

recipeRow.AutoApplyModuleTemplate(Project.current.sharedModuleTemplates);
recipeRow.AutoApplySingleCategoryModule();
}

private static EntityCrafter? GetSelectedFuelCrafter(RecipeOrTechnology recipe, IObjectWithQuality<Goods>? selectedFuel) =>
Expand Down
62 changes: 62 additions & 0 deletions Yafc.Model/Model/ProductionTableContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,68 @@ public void AutoApplyModuleTemplate(List<ProjectModuleTemplate> moduleTemplates)
}
}

/// <summary>
/// Apply a default module when this row uses a single-category module building and no template was selected.
/// The chosen module is based upon the unlock order and what milestones are currently unlocked. No secondary sort on prod/speed to keep the code simpler
/// </summary>
public void AutoApplySingleCategoryModule() {
//Don't change anything if existing modules are set
if (modules != null) {
return;
}

//Basic safety checks
if (recipe is null || entity is null || entity.target.moduleSlots <= 0) {
return;
}

if (recipe.target is not Recipe selectedRecipe) {
Comment thread
veger marked this conversation as resolved.
Outdated
return;
}

if (entity.target.allowedModuleCategories is not [string moduleCategory]) {
return;
}

Bits recipeUnlockOrder = DataUtils.GetMilestoneOrder(selectedRecipe.id);

IObjectWithQuality<Module>? bestModule = null;
Bits bestUnlockOrder = default;

//Loop over all modules finding the one that is usable, unlocked, and unlocks latest according to milestones
foreach (Module module in Database.allModules) {
//Check module is in the building's allowed category
if (!string.Equals(module.moduleSpecification.category, moduleCategory, StringComparison.Ordinal)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!string.Equals(module.moduleSpecification.category, moduleCategory, StringComparison.Ordinal)) {
if (module.moduleSpecification.category != moduleCategory) {

As far as I can tell, operator != is the same as doing an explicit ordinal comparison.

continue;
}

//Check the recipe allows this module
if (!entity.target.CanAcceptModule(module.moduleSpecification) || !recipe.target.CanAcceptModule(module)) {
continue;
}

//Check if this module is currently available for this recipe with the active milestones
Bits moduleUnlockOrder = DataUtils.GetMilestoneOrder(module.id);
if (moduleUnlockOrder.CompareTo(recipeUnlockOrder) > 0) {
Copy link
Copy Markdown
Collaborator

@DaleStan DaleStan May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Bits moduleUnlockOrder = DataUtils.GetMilestoneOrder(module.id);
if (moduleUnlockOrder.CompareTo(recipeUnlockOrder) > 0) {
if (!module.IsAccessibleWithCurrentMilestones()) {

Based on the comment, I think this is the test you want. This way the relative unlock order isn't relevant, just whether the module is unlocked.

continue;
}

//Update if this module is better (milestone order)
Bits moduleMilestoneOrder = Milestones.Instance.GetMilestoneResult(module.id);
if (bestModule == null || moduleMilestoneOrder.CompareTo(bestUnlockOrder) > 0) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (bestModule == null || moduleMilestoneOrder.CompareTo(bestUnlockOrder) > 0) {
if (bestModule == null || moduleMilestoneOrder > bestUnlockOrder) {

It might just be me, but I always have to spend a lot of effort interpreting calls to CompareTo

bestModule = module.With(Quality.Normal);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Quality.Normal correct here, or should it be Quality.MaxAccessible?

bestUnlockOrder = moduleMilestoneOrder;
}
}

//We have a working module, fill all the module slots
if (bestModule != null) {
modules = new ModuleTemplateBuilder {
list = [(bestModule, 0)]
}.Build(this);
}
}

public float DetermineFlow(IObjectWithQuality<Goods> goods) {
if (recipe is null) {
throw new InvalidOperationException("Cannot determine flow when no recipe is selected.");
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Version:
Date:
Features:
- Improve early-startup error logging
- Automatically apply single use modules to recipes. e.g. Autoamtically fill PyAL buildings with appropriate module for the current milestone
Fixes:
----------------------------------------------------------------------------------------------------------------------
Version: 2.18.1
Expand Down