From 3aba901fd9a3f82fe98ae1f208ff9a9397ba38ff Mon Sep 17 00:00:00 2001
From: protocol_1903 <67478786+protocol-1903@users.noreply.github.com>
Date: Tue, 23 Jun 2026 18:43:50 -0700
Subject: [PATCH 1/5] update for 2.1
---
changelog.txt | 12 +
data-final-fixes.lua | 38 +--
data-updates.lua | 12 +-
info.json | 8 +-
lib/autorecipes.lua | 21 +-
lib/data-stage.lua | 9 +-
lib/lib.lua | 10 +-
lib/metas/entity.lua | 26 +-
lib/metas/fluid.lua | 6 +-
lib/metas/item.lua | 23 +-
lib/metas/metas.lua | 16 +-
lib/metas/recipe.lua | 261 +++++++++---------
lib/metas/technology.lua | 41 +--
lib/metas/tile.lua | 6 +-
lib/pipe-connections.lua | 17 +-
.../compatibility/reverse-factory.lua | 8 +-
.../compatibility/transport-drones.lua | 2 +-
.../functions/compatibility/yirailway.lua | 10 +-
prototypes/yafc.lua | 4 +-
tests/control.lua | 3 +-
tests/data.lua | 8 +-
tests/scenario-tests.lua | 185 +++++++------
22 files changed, 350 insertions(+), 376 deletions(-)
diff --git a/changelog.txt b/changelog.txt
index 67daa5c..561320e 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,4 +1,16 @@
---------------------------------------------------------------------------------------------------
+Version: 3.1.0
+Date: ???
+ Changes:
+ - Updated to Factorio 2.1
+ - Removed RECIPE.change_category, added add_category, remove_category, replace_category, has_category, and has_categories
+ - Updated annotations for emmylua integration
+ - Updated py.farm_speed and py.farm_speed_derived to use 0-indexed module slots with a building at native -100% speed
+ - Updated certain tests for new prototype properties
+ - Updated global checker
+ - Updated py.pipe_pictures to load graphics definitions directly from base instead of inferring them, allowing more to be used
+ - Fixed a potential crash when no tools are defined
+---------------------------------------------------------------------------------------------------
Version: 3.0.42
Date: 2026-05-27
Changes:
diff --git a/data-final-fixes.lua b/data-final-fixes.lua
index 372ddac..af10eb6 100644
--- a/data-final-fixes.lua
+++ b/data-final-fixes.lua
@@ -22,16 +22,7 @@ end
if feature_flags.spoiling then
local spoilage_loops = {} -- all known loops
local spoilage_chains = {} -- all known
- for _, prototype in pairs{
- "item",
- "ammo",
- "capsule",
- "gun",
- "module",
- "tool",
- "armor",
- "repair-tool"
- } do for _, spoiler in pairs(data.raw[prototype]) do
+ for _, prototype in pairs(defines.prototypes.item) do for _, spoiler in pairs(data.raw[prototype] or {}) do
if spoiler.spoil_result then
local spoil_result, spoil_chain, last_checked = find_base_spoil_result(spoiler, {})
if spoil_result then -- spoilage chain found, set accordingly
@@ -113,7 +104,6 @@ local signal_recipes = {
local create_signal_mode = settings.startup["pypp-extended-recipe-signals"].value
for _, recipe in pairs(data.raw.recipe) do
- recipe.always_show_products = true
recipe.always_show_made_in = true
if not recipe.maximum_productivity then recipe.maximum_productivity = 1000000 end -- Disable the max productivity cap
@@ -194,7 +184,7 @@ if create_signal_mode then
if #alternatives > 1 then
for _, recipe in pairs(alternatives) do
-- Skip recipe categories where signals aren't useful for any recipe
- if (recipe.category and (recipe.category.name == "compost" or recipe.category.name == "py-barreling")) then
+ if (recipe:has_category("compost") or recipe:has_category("py-barreling")) then
break
end
-- Determine amount of main product to display in signal name
@@ -202,7 +192,7 @@ if create_signal_mode then
local main_product_name = recipe:get_main_product(true).name
for _, result in pairs(recipe.results) do
if result.name == main_product_name then
- if result.probability and result.probability < 1 then
+ if result.independent_probability and result.independent_probability < 1 then
-- Some recipes have random amount for multiples, such as nuclear isotopes.
local prob_amt = 0
if result.amount then
@@ -210,7 +200,7 @@ if create_signal_mode then
elseif result.amount_min and result.amount_max then
prob_amt = (result.amount_min + result.amount_max) / 2
end
- amt = result.probability * prob_amt
+ amt = result.independent_probability * prob_amt
break
elseif result.amount_min and result.amount_max then
amt = (result.amount_min + result.amount_max) / 2
@@ -223,7 +213,6 @@ if create_signal_mode then
end
-- Inject recipe output into each localised name parameter, since native output display is not consistently shown
if recipe.localised_name[1] == "?" then
- recipe.show_amount_in_title = false
for i, name in pairs(recipe.localised_name) do
if i > 1 and amt ~= 1 then
recipe.localised_name[i] = {"recipe-name.recipe-amount", tostring(amt), name}
@@ -406,9 +395,9 @@ end
for _, lab in pairs(data.raw.lab) do
table.sort(lab.inputs, function(i1, i2)
- local science_pack_a = data.raw.tool[i1]
+ local science_pack_a = ITEM(i1)
if not science_pack_a then error("Missing science pack prototype " .. i1 .. " in lab " .. lab.name) end
- local science_pack_b = data.raw.tool[i2]
+ local science_pack_b = ITEM(i2)
if not science_pack_b then error("Missing science pack prototype " .. i2 .. " in lab " .. lab.name) end
return science_pack_a.order < science_pack_b.order
end)
@@ -556,4 +545,19 @@ if not (mods.declutter or mods.autotech) then
end
end
+-- cleanup
+if not mods.autotech then
+ for _, type in pairs(data.raw) do
+ for _, prototype in pairs(type) do
+ prototype.autotech_ignore = nil
+ prototype.autotech_always_available = nil
+ if prototype.results then
+ for _, result in pairs(prototype.results) do
+ result.autotech_is_not_primary_source = nil
+ end
+ end
+ end
+ end
+end
+
if settings.startup["pypp-tests"].value then require "tests.data" end
diff --git a/data-updates.lua b/data-updates.lua
index ae8a13a..89bc1cb 100644
--- a/data-updates.lua
+++ b/data-updates.lua
@@ -22,8 +22,8 @@ local function set_underground_recipe(underground, belt, prev_underground, prev_
end
end
- if fluid and (RECIPE(underground).category or "crafting") == "crafting" then
- RECIPE(underground):set_fields {category = "crafting-with-fluid"}
+ if fluid then
+ RECIPE(underground):remove_category("crafting"):add_category("crafting-with-fluid")
end
end
@@ -37,10 +37,10 @@ set_underground_recipe("fast-underground-belt", "fast-transport-belt", "undergro
set_underground_recipe("express-underground-belt", "express-transport-belt", "fast-underground-belt", "fast-transport-belt")
local big_recipe_icons_blacklist = {
- -- ["rc-mk01"] = true,
- -- ["rc-mk02"] = true,
- -- ["rc-mk03"] = true,
- -- ["rc-mk04"] = true,
+ ["rc-mk01"] = true,
+ ["rc-mk02"] = true,
+ ["rc-mk03"] = true,
+ ["rc-mk04"] = true,
}
for _, prototype in pairs {"assembling-machine", "furnace", "container", "logistic-container"} do
diff --git a/info.json b/info.json
index 37fae80..f511520 100644
--- a/info.json
+++ b/info.json
@@ -1,14 +1,14 @@
{
"name": "pypostprocessing",
- "version": "3.0.42",
- "factorio_version": "2.0",
+ "version": "3.1.0",
+ "factorio_version": "2.1",
"title": "Pyanodons Post-processing",
- "author": "Pyanodon, Shadowglass, LambdaLemon",
+ "author": "Pyanodon, Shadowglass, LambdaLemon, protocol_1903",
"contact": "https://discord.gg/SBHM3h5Utj",
"homepage": "https://mods.factorio.com/mods/pyanodon/pytech",
"description": "Post-processing steps for Pyanodons modpack. Overhauls the technology tree to make sure prerequisites are set based on unlocked recipes.",
"dependencies": [
- "base >= 2.0.48",
+ "base >= 2.1.0",
"? quality",
"? elevated-rails",
"? space-age",
diff --git a/lib/autorecipes.lua b/lib/autorecipes.lua
index 50950fb..b23431e 100644
--- a/lib/autorecipes.lua
+++ b/lib/autorecipes.lua
@@ -2,7 +2,7 @@
--[[
py.autorecipes { -- is a function call can be many per file is the same as RECIPE{} that is used in the rest of pymods
name = 'single-example', -- recipe name if in single recipe mode *@*
- category = 'recipe-category', -- used in input recipe and output if outcategory not provided to set category
+ categories = {'recipe-category'}, -- used in input recipe and output if outcategory not provided to set category
singlerecipe = false, --=true: its a single recipe done 1 machine. takes ingredients and outputs the results. --=false: creates 2 recipes. 1 with the ingredients as inputs and outputs an item. 2nd recipe takes the item in and outputs the results.
module_limitations = "ulric", --adds the recipes to a modules allowed recipes table *
subgroup = 'subgroup', -- sets the recipes subgroups for menu organizion
@@ -18,7 +18,7 @@ py.autorecipes { -- is a function call can be many per file is the same as RECIP
},
results = -- double duh, same as ingredients first time cant be empty or you get nothing
{
- {name='bones', amount = 'amount'*('*!*'),probability = 'probability'**, amount_min = 'amount_min'**'***', amount_max = 'amount_max'**'***'},
+ {name='bones', amount = 'amount'*('*!*'),independent_probability = 'independent_probability'**, amount_min = 'amount_min'**'***', amount_max = 'amount_max'**'***'},
{'result'}, -- see above for details
{'result'}, -- again not limited by this code to number of results
},
@@ -86,12 +86,11 @@ local function modify_recipe_tables(item, items_table, previous_item_names, resu
elseif type(item.fallback) == "table" and item.fallback.name then
name = item.fallback.name
item.name = name
- if item.fallback.amount then
- item.amount = item.fallback.amount
- end
+ item.amount = item.fallback.amount
elseif data.raw.fluid[barrel] then
name = item.name
end
+ item.fallback = nil -- remove unnecessary data
if previous_item_names[name] ~= true then
local item_type
@@ -165,6 +164,8 @@ local function modify_recipe_tables(item, items_table, previous_item_names, resu
for _, pre in pairs(items_table) do
if pre.name == name then
pre.amount = item.amount
+ pre.amount_min = nil
+ pre.amount_max = nil
end
end
end
@@ -183,6 +184,7 @@ local function modify_recipe_tables(item, items_table, previous_item_names, resu
end
return_item = {type = item_type, name = name, amount = amount}
table.insert(result_table, return_item)
+ item.return_item = nil
end
if item.return_barrel then
@@ -208,6 +210,7 @@ local function modify_recipe_tables(item, items_table, previous_item_names, resu
end
table.insert(result_table, barrels_to_return)
::already_had_barrel_result::
+ item.return_barrel = nil
end
end
@@ -243,7 +246,7 @@ local function recipe_item_builder(ingredients, results, previous_ingredients, p
end
---Provides an interface to quickly build tiered recipes. See recipes-auto-brains.lua for an example
----@param params {name:RecipeID,category:RecipeCategoryID,subgroup:data.ItemSubGroupID,order:data.Order,main_product?:string,crafting_speed:double,allowed_module_categories:[data.ModuleCategoryID],number_icons:boolean,mats:[{name?:string,ingredients?:[data.IngredientPrototype],results?:[data.ProductPrototype],crafting_speed?:double,tech?:TechnologyID,icon?:data.FileName,icon_size?:integer,icons?:[data.IconData],main_product?:string}]}
+---@param params {name:RecipeID,categories:RecipeCategoryID[],subgroup:data.ItemSubGroupID,order:data.Order,main_product?:string,crafting_speed:double,allowed_module_categories:[data.ModuleCategoryID],number_icons:boolean,mats:[{name?:string,ingredients?:[data.IngredientPrototype],results?:[data.ProductPrototype],crafting_speed?:double,tech?:TechnologyID,icon?:data.FileName,icon_size?:integer,icons?:[data.IconData],main_product?:string}]}
py.autorecipes = function(params)
local previous_ingredients = {}
local previous_results = {}
@@ -264,7 +267,7 @@ py.autorecipes = function(params)
local recipe = RECIPE {
type = "recipe",
name = recipe_name,
- category = params.category,
+ categories = params.categories,
enabled = tier.tech == nil,
energy_required = tier.crafting_speed or params.crafting_speed,
ingredients = fixed_ingredients,
@@ -274,7 +277,7 @@ py.autorecipes = function(params)
allowed_module_categories = params.allowed_module_categories,
icons = tier.icons,
main_product = tier.main_product or params.main_product,
- allow_productivity = params.category ~= "slaughterhouse",
+ allow_productivity = not table.any(params.categories, "slaughterhouse"),
}
if tier.tech then recipe:add_unlock(tier.tech) end
if params.number_icons then -- add numbers to farming recipes so that they're not identical
@@ -300,7 +303,7 @@ py.autorecipes = function(params)
local scale = (data.raw.recipe[recipe_name].icons[1].scale or .5) / 2
table.insert(
data.raw.recipe[recipe_name].icons,
- {icon = "__pyalienlifegraphics__/graphics/icons/" .. i .. ".png", scale = scale, shift = {32 * scale, 32 * scale}, floating = true}
+ {icon = "__pyalienlifegraphics__/graphics/icons/" .. i .. ".png", scale = scale, shift = {36 * scale, 36 * scale}, floating = true}
)
elseif tier.icon then
data.raw.recipe[recipe_name].icon = tier.icon
diff --git a/lib/data-stage.lua b/lib/data-stage.lua
index 25f345f..4cd1ea2 100644
--- a/lib/data-stage.lua
+++ b/lib/data-stage.lua
@@ -151,7 +151,7 @@ end
function py.farm_speed(num_slots, desired_speed, module_bonus)
module_bonus = module_bonus or 1
-- mk1 modules are 100% bonus speed * module_bonus. The farm itself then counts as much as one module
- return desired_speed / (num_slots + 1 / module_bonus) / module_bonus
+ return desired_speed / (num_slots / module_bonus) / module_bonus
end
---Returns the correct farm speed for a mk2+ farm based on the number of modules and the mk1 speed.
@@ -165,10 +165,9 @@ function py.farm_speed_derived(num_slots, base_entity_name, base_module_bonus, t
base_module_bonus = base_module_bonus or 1
local e = data.raw["assembling-machine"][base_entity_name]
local mk1_slots = e.module_slots
- local desired_mk1_speed = e.crafting_speed * (mk1_slots * base_module_bonus + 1)
- local speed_improvement_ratio = num_slots / mk1_slots
- this_bonus = this_bonus or speed_improvement_ratio * base_module_bonus
- return (desired_mk1_speed * speed_improvement_ratio) / (num_slots + 1 / this_bonus) / base_module_bonus
+ local desired_mk1_speed = e.crafting_speed * mk1_slots * base_module_bonus
+ this_bonus = this_bonus or base_module_bonus * num_slots / mk1_slots
+ return desired_mk1_speed * this_bonus / (num_slots * base_module_bonus)
end
---Returns a composite icon with a base icon and up to 4 child icons.
diff --git a/lib/lib.lua b/lib/lib.lua
index 59cf8c3..83bdbf6 100644
--- a/lib/lib.lua
+++ b/lib/lib.lua
@@ -212,6 +212,13 @@ local pyae_globals = {
"Solar",
"Wind",
"Aerial",
+ "Tidal"
+}
+
+local debugadapter_globals = {
+ -- data stage
+ "setfenv",
+ -- control stage
}
function py.has_any_py_mods()
@@ -343,7 +350,8 @@ local global_vars = table.array_combine(
pypp_globals,
spidertron_enhancements_globals,
pycp_globals,
- pyae_globals
+ pyae_globals,
+ debugadapter_globals
)
for _, var in pairs(global_vars) do
diff --git a/lib/metas/entity.lua b/lib/metas/entity.lua
index e3408c6..14f056f 100644
--- a/lib/metas/entity.lua
+++ b/lib/metas/entity.lua
@@ -1,14 +1,15 @@
+---@diagnostic disable-next-line: unresolved-require
local collision_mask_util = require "__core__/lualib/collision-mask-util"
local entity_types = defines.prototypes.entity
----@class data.EntityPrototype
----@field public standardize fun(self: data.EntityPrototype): data.EntityPrototype
----@field public add_flag fun(self: data.EntityPrototype, flag: string): data.EntityPrototype, boolean
----@field public remove_flag fun(self: data.EntityPrototype, flag: string): data.EntityPrototype, boolean
----@field public has_flag fun(self: data.EntityPrototype, flag: string): boolean
+---@class pYdata.EntityPrototype:pYdata.AnyPrototype,data.EntityPrototype
+---@operator call(string|pYdata.EntityPrototype|data.EntityPrototype): pYdata.EntityPrototype
+---@field public standardize fun(self: pYdata.EntityPrototype): pYdata.EntityPrototype
+---@field public add_flag fun(self: pYdata.EntityPrototype, flag: string): pYdata.EntityPrototype, boolean
+---@field public remove_flag fun(self: pYdata.EntityPrototype, flag: string): pYdata.EntityPrototype, boolean
+---@field public has_flag fun(self: pYdata.EntityPrototype, flag: string): boolean
ENTITY = setmetatable({}, {
- ---@param entity data.EntityPrototype
__call = function(self, entity)
local etype = type(entity)
if etype == "string" then
@@ -37,6 +38,8 @@ ENTITY = setmetatable({}, {
end
})
+---@diagnostic disable-next-line: missing-fields
+---@type pYdata.EntityPrototype
local metas = {}
metas.standardize = function(self)
@@ -58,10 +61,6 @@ metas.standardize = function(self)
return self
end
----@param self data.EntityPrototype
----@param flag string
----@return data.EntityPrototype self
----@return boolean success
metas.add_flag = function(self, flag)
self.flags = self.flags or {}
for _, f in pairs(self.flags) do
@@ -73,10 +72,6 @@ metas.add_flag = function(self, flag)
return self, true -- flag added
end
----@param self data.EntityPrototype
----@param flag string
----@return data.EntityPrototype self
----@return boolean success
metas.remove_flag = function(self, flag)
if not self.flags then return self, false end
for i, f in pairs(self.flags) do
@@ -88,9 +83,6 @@ metas.remove_flag = function(self, flag)
return self, false -- could not find flag
end
----@param self data.EntityPrototype
----@param flag string
----@return boolean has_flag
metas.has_flag = function(self, flag)
if not self.flags then return false end
for _, f in pairs(self.flags) do
diff --git a/lib/metas/fluid.lua b/lib/metas/fluid.lua
index 84452d3..77f20ef 100644
--- a/lib/metas/fluid.lua
+++ b/lib/metas/fluid.lua
@@ -1,6 +1,6 @@
----@class data.FluidPrototype
+---@class pYdata.FluidPrototype:pYdata.AnyPrototype,data.FluidPrototype
+---@operator call(string|pYdata.FluidPrototype|data.FluidPrototype): pYdata.FluidPrototype
FLUID = setmetatable(data.raw.fluid, {
- ---@param fluid data.FluidPrototype
__call = function(self, fluid)
local ftype = type(fluid)
if ftype == "string" then
@@ -16,6 +16,8 @@ FLUID = setmetatable(data.raw.fluid, {
end
})
+---@diagnostic disable-next-line: missing-fields
+---@type pYdata.FluidPrototype
local metas = {}
return metas
diff --git a/lib/metas/item.lua b/lib/metas/item.lua
index 70860b7..8aa70d3 100644
--- a/lib/metas/item.lua
+++ b/lib/metas/item.lua
@@ -1,10 +1,11 @@
local item_prototypes = defines.prototypes.item
----@class data.ItemPrototype
+---@class pYdata.ItemPrototype:pYdata.AnyPrototype,data.ItemPrototype
+---@operator call(string|pYdata.ItemPrototype|data.ItemPrototype): pYdata.ItemPrototype
---@field public add_flag fun(self: data.ItemPrototype, flag: string): data.ItemPrototype, boolean
---@field public remove_flag fun(self: data.ItemPrototype, flag: string): data.ItemPrototype, boolean
---@field public has_flag fun(self: data.ItemPrototype, flag: string): boolean
----@field public spoil fun(self: data.ItemPrototype, spoil_result: (string | table), spoil_ticks: number): data.ItemPrototype, boolean
+---@field public spoil fun(self: data.ItemPrototype, spoil_result: (string | table), spoil_ticks: uint): data.ItemPrototype, boolean
ITEM = setmetatable({}, {
---@param item data.ItemPrototype
__call = function(self, item)
@@ -35,12 +36,10 @@ ITEM = setmetatable({}, {
end
})
+---@diagnostic disable-next-line: missing-fields
+---@type pYdata.ItemPrototype
local metas = {}
----@param self data.ItemPrototype
----@param flag string
----@return data.ItemPrototype self
----@return boolean success
metas.add_flag = function(self, flag)
self.flags = self.flags or {}
for _, f in pairs(self.flags) do
@@ -52,10 +51,6 @@ metas.add_flag = function(self, flag)
return self, true -- flag added
end
----@param self data.ItemPrototype
----@param flag string
----@return data.ItemPrototype self
----@return boolean success
metas.remove_flag = function(self, flag)
if not self.flags then return self, false end
for i, f in pairs(self.flags) do
@@ -67,9 +62,6 @@ metas.remove_flag = function(self, flag)
return self, false -- could not find flag
end
----@param self data.ItemPrototype
----@param flag string
----@return boolean has_flag
metas.has_flag = function(self, flag)
if not self.flags then return false end
for _, f in pairs(self.flags) do
@@ -102,11 +94,6 @@ py.spoil_triggers = {
end
}
----@param self data.ItemPrototype
----@param spoil_result string|data.SpoilToTriggerResult
----@param spoil_ticks int
----@return table self
----@return boolean success
metas.spoil = function(self, spoil_result, spoil_ticks)
if not feature_flags.spoiling then return self, false end -- spoilage is off
if not spoil_ticks then error("No spoil ticks provided for item " .. self.name) end
diff --git a/lib/metas/metas.lua b/lib/metas/metas.lua
index 528262f..f0ff72f 100644
--- a/lib/metas/metas.lua
+++ b/lib/metas/metas.lua
@@ -9,14 +9,14 @@ local lib = {
tile = require "tile"
}
----@class data.AnyPrototype
----@field public copy fun(self: data.AnyPrototype, new_name: (string | fun(self: data.AnyPrototype): string)?): data.AnyPrototype
----@field public subgroup_order fun(self: data.AnyPrototype, subgroup: string, order: string): data.AnyPrototype
----@field public set_fields fun(self: data.AnyPrototype, fields: table): data.AnyPrototype
----@field public set fun(self: data.AnyPrototype, field: string, value: any): data.AnyPrototype
----@field public delete fun(self: data.AnyPrototype)
----@field public hide fun(self: data.AnyPrototype): data.AnyPrototype
----@field public unhide fun(self: data.AnyPrototype): data.AnyPrototype
+---@class pYdata.AnyPrototype:data.AnyPrototype
+---@field public copy fun(self: pYdata.AnyPrototype, new_name: (string | fun(self: pYdata.AnyPrototype): string)?): pYdata.AnyPrototype
+---@field public subgroup_order fun(self: pYdata.AnyPrototype, subgroup: string, order: string): pYdata.AnyPrototype
+---@field public set_fields fun(self: pYdata.AnyPrototype, fields: table): pYdata.AnyPrototype
+---@field public set fun(self: pYdata.AnyPrototype, field: string, value: any): pYdata.AnyPrototype
+---@field public delete fun(self: pYdata.AnyPrototype)
+---@field public hide fun(self: pYdata.AnyPrototype): pYdata.AnyPrototype
+---@field public unhide fun(self: pYdata.AnyPrototype): pYdata.AnyPrototype
for _, meta in pairs(lib) do
meta.copy = function(self, new_name)
diff --git a/lib/metas/recipe.lua b/lib/metas/recipe.lua
index d1c5236..7cf78b3 100644
--- a/lib/metas/recipe.lua
+++ b/lib/metas/recipe.lua
@@ -2,33 +2,41 @@ local table_insert = table.insert
--unsafe functions overrides protections for ingredient/result existence
+---@diagnostic disable-next-line: missing-fields
+---@type pYdata.RecipePrototype
local metas = {}
----@class data.RecipePrototype
----@field public standardize fun(self: data.RecipePrototype): data.RecipePrototype
----@field public add_unlock fun(self: data.RecipePrototype, technology_name: string | string[]): data.RecipePrototype, boolean
----@field public remove_unlock fun(self: data.RecipePrototype, technology_name: string | string[]): data.RecipePrototype, boolean
----@field public replace_unlock fun(self: data.RecipePrototype, technology_old: string | string[], technology_new: string | string[]): data.RecipePrototype, boolean
----@field public replace_ingredient fun(self: data.RecipePrototype, old_ingredient: string, new_ingredient: string | data.IngredientPrototype, new_amount: integer?): data.RecipePrototype, boolean
----@field public replace_ingredient_unsafe fun(self: data.RecipePrototype, old_ingredient: string, new_ingredient: string | data.IngredientPrototype, new_amount: integer?): data.RecipePrototype, boolean
----@field public add_ingredient fun(self: data.RecipePrototype, ingredient: data.IngredientPrototype): data.RecipePrototype, boolean
----@field public add_ingredient_unsafe fun(self: data.RecipePrototype, ingredient: data.IngredientPrototype): data.RecipePrototype, boolean
----@field public remove_ingredient fun(self: data.RecipePrototype, ingredient_name: string): data.RecipePrototype, integer
----@field public replace_result fun(self: data.RecipePrototype, old_result: string, new_result: string | data.ProductPrototype, new_amount: integer?): data.RecipePrototype, boolean
----@field public replace_result_unsafe fun(self: data.RecipePrototype, old_result: string, new_result: string | data.ProductPrototype, new_amount: integer?): data.RecipePrototype, boolean
----@field public add_result fun(self: data.RecipePrototype, result: data.ProductPrototype): data.RecipePrototype, boolean
----@field public remove_result fun(self: data.RecipePrototype, result_name: string): data.RecipePrototype, integer
----@field public clear_ingredients fun(self: data.RecipePrototype): data.RecipePrototype, boolean
----@field public multiply_result_amount fun(self: data.RecipePrototype, result_name: string, percent: number): data.RecipePrototype, boolean
----@field public multiply_ingredient_amount fun(self: data.RecipePrototype, ingredient_name: string, percent: number): data.RecipePrototype, boolean
----@field public add_result_amount fun(self: data.RecipePrototype, result_name: string, increase: number): data.RecipePrototype, boolean
----@field public add_ingredient_amount fun(self: data.RecipePrototype, ingredient_name: string, increase: number): data.RecipePrototype, boolean
----@field public set_result_amount fun(self: data.RecipePrototype, result_name: string, amount: number): data.RecipePrototype, boolean
----@field public set_ingredient_amount fun(self: data.RecipePrototype, ingredient_name: string, amount: number): data.RecipePrototype, boolean
----@field public get_main_product fun(self: data.RecipePrototype, allow_multi_product: boolean?): LuaItemPrototype?|LuaFluidPrototype?
----@field public get_icons fun(self: data.RecipePrototype): data.IconData[]
+---@class pYdata.RecipePrototype:pYdata.AnyPrototype,data.RecipePrototype
+---@operator call(string|pYdata.RecipePrototype|data.RecipePrototype): pYdata.RecipePrototype
+---@field public standardize fun(self: pYdata.RecipePrototype): pYdata.RecipePrototype
+---@field public add_unlock fun(self: pYdata.RecipePrototype, technology_name: string | string[]): pYdata.RecipePrototype, boolean
+---@field public remove_unlock fun(self: pYdata.RecipePrototype, technology_name: string | string[]): pYdata.RecipePrototype, boolean
+---@field public replace_unlock fun(self: pYdata.RecipePrototype, technology_old: string | string[], technology_new: string | string[]): pYdata.RecipePrototype, boolean
+---@field public replace_ingredient fun(self: pYdata.RecipePrototype, old_ingredient: string, new_ingredient: string | data.IngredientPrototype, new_amount: integer?): pYdata.RecipePrototype, boolean
+---@field public replace_ingredient_unsafe fun(self: pYdata.RecipePrototype, old_ingredient: string, new_ingredient: string | data.IngredientPrototype, new_amount: integer?): pYdata.RecipePrototype, boolean
+---@field public add_ingredient fun(self: pYdata.RecipePrototype, ingredient: data.IngredientPrototype): pYdata.RecipePrototype, boolean
+---@field public add_ingredient_unsafe fun(self: pYdata.RecipePrototype, ingredient: data.IngredientPrototype): pYdata.RecipePrototype, boolean
+---@field public remove_ingredient fun(self: pYdata.RecipePrototype, ingredient_name: string): pYdata.RecipePrototype, integer
+---@field public replace_result fun(self: pYdata.RecipePrototype, old_result: string, new_result: string | data.ProductPrototype, new_amount: integer?): pYdata.RecipePrototype, boolean
+---@field public replace_result_unsafe fun(self: pYdata.RecipePrototype, old_result: string, new_result: string | data.ProductPrototype, new_amount: integer?): pYdata.RecipePrototype, boolean
+---@field public add_result fun(self: pYdata.RecipePrototype, result: data.ProductPrototype): pYdata.RecipePrototype, boolean
+---@field public remove_result fun(self: pYdata.RecipePrototype, result_name: string): pYdata.RecipePrototype, integer
+---@field public clear_ingredients fun(self: pYdata.RecipePrototype): pYdata.RecipePrototype, boolean
+---@field public clear_results fun(self: pYdata.RecipePrototype): pYdata.RecipePrototype, boolean
+---@field public multiply_result_amount fun(self: pYdata.RecipePrototype, result_name: string, percent: number): pYdata.RecipePrototype, boolean
+---@field public multiply_ingredient_amount fun(self: pYdata.RecipePrototype, ingredient_name: string, percent: number): pYdata.RecipePrototype, boolean
+---@field public add_result_amount fun(self: pYdata.RecipePrototype, result_name: string, increase: number): pYdata.RecipePrototype, boolean
+---@field public add_ingredient_amount fun(self: pYdata.RecipePrototype, ingredient_name: string, increase: number): pYdata.RecipePrototype, boolean
+---@field public set_result_amount fun(self: pYdata.RecipePrototype, result_name: string, amount: number): pYdata.RecipePrototype, boolean
+---@field public set_ingredient_amount fun(self: pYdata.RecipePrototype, ingredient_name: string, amount: number): pYdata.RecipePrototype, boolean
+---@field public add_category fun(self: pYdata.RecipePrototype, category_name: data.RecipeCategoryID): pYdata.RecipePrototype, boolean
+---@field public remove_category fun(self: pYdata.RecipePrototype, category_name: data.RecipeCategoryID): pYdata.RecipePrototype, boolean
+---@field public replace_category fun(self: pYdata.RecipePrototype, old: data.RecipeCategoryID, new: data.RecipeCategoryID): pYdata.RecipePrototype, boolean
+---@field public has_category fun(self: pYdata.RecipePrototype, category_name: data.RecipeCategoryID): boolean
+---@field public has_categories fun(self: pYdata.RecipePrototype, category_name: data.RecipeCategoryID[], all?: boolean): boolean # Returns true if the recipe has any of the categories. all? categories must match to pass
+---@field public get_main_product fun(self: pYdata.RecipePrototype, allow_multi_product: boolean?): LuaItemPrototype?|LuaFluidPrototype?
+---@field public get_icons fun(self: pYdata.RecipePrototype): data.IconData[]
RECIPE = setmetatable(data.raw.recipe, {
- ---@param recipe data.RecipePrototype
__call = function(self, recipe)
local rtype = type(recipe)
if rtype == "string" then
@@ -80,10 +88,6 @@ py.allow_productivity = function(recipe_names)
end
end
----@param self data.RecipePrototype
----@param technology_name string|string[]
----@return data.RecipePrototype self
----@return boolean success
metas.add_unlock = function(self, technology_name)
if type(technology_name) == "table" then
local success = true
@@ -116,10 +120,6 @@ metas.add_unlock = function(self, technology_name)
return self, true
end
----@param self data.RecipePrototype
----@param technology_name string|string[]
----@return data.RecipePrototype self
----@return boolean success
metas.remove_unlock = function(self, technology_name)
if type(technology_name) == "table" then
local success = true
@@ -148,11 +148,6 @@ metas.remove_unlock = function(self, technology_name)
return self, false -- recipe not part of tech
end
----@param self data.RecipePrototype
----@param technology_old string|string[]
----@param technology_new string|string[]
----@return data.RecipePrototype self
----@return boolean success
metas.replace_unlock = function(self, technology_old, technology_new)
local _, success_remove = self:remove_unlock(technology_old)
local _, success_add = self:add_unlock(technology_new)
@@ -198,65 +193,37 @@ do
return true -- must have been a success! cant early return on success because of possible repeated results
end
- ---@param self data.RecipePrototype
- ---@param old_ingredient string
- ---@param new_ingredient string|data.IngredientPrototype
- ---@param new_amount? int
- ---@return data.RecipePrototype self
- ---@return boolean success
metas.replace_ingredient = function(self, old_ingredient, new_ingredient, new_amount)
self:standardize()
local success = replacement_helper(self, self.ingredients, old_ingredient, new_ingredient, new_amount, false)
return self, success
end
- ---@param self data.RecipePrototype
- ---@param old_ingredient string
- ---@param new_ingredient string|data.IngredientPrototype
- ---@param new_amount? int
- ---@return data.RecipePrototype self
- ---@return boolean success
metas.replace_ingredient_unsafe = function(self, old_ingredient, new_ingredient, new_amount)
self:standardize()
local success = replacement_helper(self, self.ingredients, old_ingredient, new_ingredient, new_amount, true)
return self, success
end
- ---@param self data.RecipePrototype
- ---@param old_result string
- ---@param new_result string|data.ProductPrototype
- ---@param new_amount? int
- ---@return data.RecipePrototype self
- ---@return boolean success
metas.replace_result = function(self, old_result, new_result, new_amount)
self:standardize()
local success = replacement_helper(self, self.results, old_result, new_result, new_amount, false)
if self.main_product == old_result then
- self.main_product = type(new_result) == "string" and new_result or new_result[1] or new_result.name
+ self.main_product = type(new_result) == "string" and new_result or new_result.name
end
return self, success
end
- ---@param self data.RecipePrototype
- ---@param old_result string
- ---@param new_result string|data.ProductPrototype
- ---@param new_amount? int
- ---@return data.RecipePrototype self
- ---@return boolean success
metas.replace_result_unsafe = function(self, old_result, new_result, new_amount)
self:standardize()
local success = replacement_helper(self, self.results, old_result, new_result, new_amount, true)
if self.main_product == old_result then
- self.main_product = type(new_result) == "string" and new_result or new_result[1] or new_result.name
+ self.main_product = type(new_result) == "string" and new_result or new_result.name
end
return self, success
end
end
----@param self data.RecipePrototype
----@param ingredient data.IngredientPrototype
----@return data.RecipePrototype self
----@return boolean success
metas.add_ingredient_unsafe = function(self, ingredient)
self:standardize()
-- Ensure that this ingredient does not already exist in this recipe.
@@ -271,18 +238,14 @@ metas.add_ingredient_unsafe = function(self, ingredient)
end
end
- if (not self.category or self.category == "crafting") and ingredient.type == "fluid" then
- self.category = "crafting-with-fluid"
+ if ingredient.type == "fluid" and self:has_category("crafting") then
+ self:replace_category("crafting", "crafting-with-fluid")
end
table_insert(self.ingredients, ingredient)
return self, true
end
----@param self data.RecipePrototype
----@param ingredient data.IngredientPrototype
----@return data.RecipePrototype self
----@return boolean success
metas.add_ingredient = function(self, ingredient)
self:standardize()
if not FLUID[ingredient.name] and not ITEM[ingredient.name] then
@@ -293,20 +256,12 @@ metas.add_ingredient = function(self, ingredient)
return metas.add_ingredient_unsafe(self, ingredient)
end
----@param self data.RecipePrototype
----@param result data.ProductPrototype
----@return data.RecipePrototype self
----@return boolean success
metas.add_result = function(self, result)
self:standardize()
table_insert(self.results, result)
return self, true
end
----@param self data.RecipePrototype
----@param ingredient_name string
----@return data.RecipePrototype self
----@return int amount_removed
metas.remove_ingredient = function(self, ingredient_name)
self:standardize()
local amount_removed = 0
@@ -320,16 +275,12 @@ metas.remove_ingredient = function(self, ingredient_name)
return self, amount_removed
end
----@param self data.RecipePrototype
----@param result_name string
----@return data.RecipePrototype self
----@return int amount_removed
metas.remove_result = function(self, result_name)
self:standardize()
local amount_removed = 0
self.results = table.filter(self.results, function(result)
if result.name == result_name then
- local amount = result.amount * (result.probability or 1) or (result.amount_min + result.amount_max) * (result.probability or 1) / 2
+ local amount = result.amount * (result.independent_probability or 1) or (result.amount_min + result.amount_max) * (result.independent_probability or 1) / 2
amount_removed = amount_removed + amount
return false
end
@@ -344,15 +295,10 @@ metas.clear_ingredients = function(self)
end
metas.clear_results = function(self)
- self.ingredients = {}
+ self.results = {}
return self, true -- impossible to fail
end
----@param self data.RecipePrototype
----@param result_name string
----@param percent float
----@return data.RecipePrototype self
----@return boolean success
metas.multiply_result_amount = function(self, result_name, percent)
self:standardize()
@@ -374,11 +320,6 @@ metas.multiply_result_amount = function(self, result_name, percent)
return self, false -- could not find result
end
----@param self data.RecipePrototype
----@param ingredient_name string
----@param percent float
----@return data.RecipePrototype self
----@return boolean success
metas.multiply_ingredient_amount = function(self, ingredient_name, percent)
self:standardize()
@@ -397,11 +338,6 @@ metas.multiply_ingredient_amount = function(self, ingredient_name, percent)
return self, false -- could not find ingredient
end
----@param self data.RecipePrototype
----@param result_name string
----@param increase int
----@return data.RecipePrototype self
----@return boolean success
metas.add_result_amount = function(self, result_name, increase)
self:standardize()
@@ -420,11 +356,6 @@ metas.add_result_amount = function(self, result_name, increase)
return self, false -- could not find result
end
----@param self data.RecipePrototype
----@param ingredient_name string
----@param increase int
----@return data.RecipePrototype self
----@return boolean success
metas.add_ingredient_amount = function(self, ingredient_name, increase)
self:standardize()
@@ -443,46 +374,107 @@ metas.add_ingredient_amount = function(self, ingredient_name, increase)
return self, false -- could not find ingredient
end
----@param self data.RecipePrototype
----@param result_name string
----@param amount int
----@return data.RecipePrototype self
----@return boolean success
metas.set_result_amount = function(self, result_name, amount)
return self:replace_result(result_name, result_name, amount)
end
----@param self data.RecipePrototype
----@param ingredient_name string
----@param amount int
----@return data.RecipePrototype self
----@return boolean success
metas.set_ingredient_amount = function(self, ingredient_name, amount)
return self:replace_ingredient(ingredient_name, ingredient_name, amount)
end
----@param self data.RecipePrototype
----@param category_name string
----@return data.RecipePrototype self
----@return boolean success
-metas.change_category = function(self, category_name)
+metas.add_category = function(self, category_name)
self:standardize()
+ self.categories = self.categories or {} -- not in self:standardize() because a recipe with no categories errors the game
- if data.raw["recipe-category"][category_name] then
- self.category = category_name
+ if not data.raw["recipe-category"][category_name] then
+ log("WARNING @ \'" .. self.name .. "\':add_category(): Category " .. category_name .. " not found")
+ return self, false -- category does not exist
+ else
+ for _, category in pairs(self.categories) do
+ if category == category_name then
+ return self, false -- category already set
+ end
+ end
+ self.categories[#self.categories+1] = category_name
return self, true -- successful set
+ end
+end
+
+metas.remove_category = function(self, category_name)
+ self:standardize()
+
+ if not data.raw["recipe-category"][category_name] then
+ log("WARNING @ \'" .. self.name .. "\':remove_category(): Category " .. category_name .. " not found")
+ return self, false -- category does not exist
else
- log("WARNING @ \'" .. self.name .. "\':change_category(): Category " .. category_name .. " not found")
+ if category_name == "crafting" and (not self.categories or #self.categories == 0) then
+ return self, true -- fake positive if trying to remove 'category' with no categories because its the default
+ end
+ for i, category in pairs(self.categories or {}) do
+ if category == category_name then
+ table.remove(self.categories, i)
+ if #self.categories == 0 then self.categories = nil end -- remove categories if it is an empty table
+ return self, true -- successfully removed
+ end
+ end
+ return self, false -- category not found
+ end
+end
+
+metas.replace_category = function(self, old, new)
+ self:standardize()
+
+ if not data.raw["recipe-category"][old] then
+ log("WARNING @ \'" .. self.name .. "\':replace_category(): Category " .. old .. " not found")
+ return self, false -- category does not exist
+ elseif not data.raw["recipe-category"][new] then
+ log("WARNING @ \'" .. self.name .. "\':replace_category(): Category " .. new .. " not found")
return self, false -- category does not exist
+ else
+ local _, success = self:remove_category(old)
+ if success then
+ return self:add_category(new) -- conditional on success of add_category
+ else
+ return self, false -- DNE, do not add
+ end
end
end
+metas.has_category = function(self, category_name)
+ self:standardize()
+
+ if not data.raw["recipe-category"][category_name] then
+ log("WARNING @ \'" .. self.name .. "\':replace_category(): Category " .. category_name .. " not found")
+ return false -- category does not exist
+ else
+ if category_name == "crafting" and (not self.categories or #self.categories == 0) then
+ return self, true -- fake positive if trying to remove 'category' with no categories because its the default
+ end
+ for _, category in pairs(self.categories or {}) do
+ if category == category_name then
+ return true -- category found
+ end
+ end
+ return false -- category not found
+ end
+end
+
+metas.has_categories = function(self, categories, all)
+ self:standardize()
+
+ for _, category in pairs(categories) do
+ if all and not self:has_category(category) then
+ return false -- all categories must be contained but this one was not
+ elseif not all and self:has_category(category) then
+ return true -- any categories must match and this one matched
+ end
+ end
+ return not not all -- all categories matched, or none did
+end
+
--- Get the prototype for the main_product using the same logic the game uses.
--- Set allow_multi_product to take the *first* result (not game behavior) instead of failing when a recipe has no main_product set but has multiple results.
---
Check https://lua-api.factorio.com/latest/prototypes/RecipePrototype.html#main_product for more details
----@param self data.RecipePrototype
----@param allow_multi_product boolean
----@return data.ItemPrototype|data.FluidPrototype?
metas.get_main_product = function(self, allow_multi_product)
self:standardize()
local target, target_type = self.main_product, "item"
@@ -504,16 +496,11 @@ metas.get_main_product = function(self, allow_multi_product)
else -- or only result
_, result = next(self.results)
end
- --[[@cast result data.ItemProductPrototype|data.ResearchProgressProductPrototype]]
+ --[[@cast result data.ItemProductPrototype]]
-- Special modding funtimes case: invalid spec
- if not (result.type == "research-progress" and result.research_item or result.name) then return end
- if result.type ~= nil and (result.type ~= "item" and result.type ~= "fluid" and result.type ~= "research-progress") then return end
+ if not result.name then return end
+ if result.type ~= nil and result.type ~= "item" and result.type ~= "fluid" then return end
target, target_type = result.name, result.type or target_type
- -- Special case: type of research-progress uses an item prototype
- if target_type == "research-progress" then
- target = result.research_item
- target_type = "item"
- end
-- Find our prototype :)
for _, category in py.iter_prototype_categories(target_type) do
local proto = category[target]
@@ -538,8 +525,6 @@ end
--- Returns the icons table a recipe would use (i.e. using the item icon if the recipe prototype has no .icons/.icon set).
--- May error on malformed prototypes.
---
Check https://lua-api.factorio.com/latest/prototypes/RecipePrototype.html#icon for more details.
----@param self data.RecipePrototype
----@return data.IconData[]
metas.get_icons = function(self)
local icon = icons(self)
if icon then return icon end
diff --git a/lib/metas/technology.lua b/lib/metas/technology.lua
index 430cbae..080ed92 100644
--- a/lib/metas/technology.lua
+++ b/lib/metas/technology.lua
@@ -1,12 +1,12 @@
----@class data.TechnologyPrototype
----@field public standardize fun(): data.TechnologyPrototype
----@field public add_prereq fun(self, prereq_technology_name: data.TechnologyID): data.TechnologyPrototype, boolean
----@field public remove_prereq fun(self, prereq_technology_name: data.TechnologyID): data.TechnologyPrototype, boolean
----@field public replace_prereq fun(self, old: data.TechnologyID, new: data.TechnologyID): data.TechnologyPrototype, boolean
----@field public remove_pack fun(self, science_pack_name: data.ItemID): data.TechnologyPrototype, boolean
----@field public add_pack fun(self, science_pack_name: data.ItemID): data.TechnologyPrototype, boolean
+---@class pYdata.TechnologyPrototype:pYdata.AnyPrototype,data.TechnologyPrototype
+---@operator call(string|pYdata.TechnologyPrototype|data.TechnologyPrototype): pYdata.TechnologyPrototype
+---@field public standardize fun(self: pYdata.TechnologyPrototype): pYdata.TechnologyPrototype
+---@field public add_prereq fun(self: pYdata.TechnologyPrototype, prereq_technology_name: data.TechnologyID): pYdata.TechnologyPrototype, boolean
+---@field public remove_prereq fun(self: pYdata.TechnologyPrototype, prereq_technology_name: data.TechnologyID): pYdata.TechnologyPrototype, boolean
+---@field public replace_prereq fun(self: pYdata.TechnologyPrototype, old: data.TechnologyID, new: data.TechnologyID): pYdata.TechnologyPrototype, boolean
+---@field public remove_pack fun(self: pYdata.TechnologyPrototype, science_pack_name: data.ItemID): pYdata.TechnologyPrototype, boolean
+---@field public add_pack fun(self: pYdata.TechnologyPrototype, science_pack_name: data.ItemID): pYdata.TechnologyPrototype, boolean
TECHNOLOGY = setmetatable(data.raw.technology, {
- ---@param technology data.TechnologyPrototype
__call = function(self, technology)
local ttype = type(technology)
if ttype == "string" then
@@ -22,9 +22,12 @@ TECHNOLOGY = setmetatable(data.raw.technology, {
end
})
+---@diagnostic disable-next-line: missing-fields
+---@type pYdata.TechnologyPrototype
local metas = {}
metas.standardize = function(self)
+ ---@diagnostic disable-next-line: assign-type-mismatch
if not self.unit and not self.research_trigger then self.unit = {ingredients = {}} end
self.prerequisites = self.prerequisites or {}
@@ -33,10 +36,6 @@ metas.standardize = function(self)
return self
end
----@param self data.TechnologyPrototype
----@param prereq_technology_name data.TechnologyID
----@return data.TechnologyPrototype self
----@return boolean success
metas.add_prereq = function(self, prereq_technology_name)
local prereq_technology = data.raw.technology[prereq_technology_name]
if not prereq_technology then
@@ -57,10 +56,6 @@ metas.add_prereq = function(self, prereq_technology_name)
return self, true -- add prereq succeeds
end
----@param self data.TechnologyPrototype
----@param prereq_technology_name data.TechnologyID
----@return data.TechnologyPrototype self
----@return boolean success
metas.remove_prereq = function(self, prereq_technology_name)
self.prerequisites = self.prerequisites or {}
@@ -75,11 +70,6 @@ metas.remove_prereq = function(self, prereq_technology_name)
end
--- Replace old prerequesite with the new one. Fails if the old one was not found.
----@param self data.TechnologyPrototype
----@param old data.TechnologyID
----@param new data.TechnologyID
----@return data.TechnologyPrototype self
----@return boolean success
metas.replace_prereq = function(self, old, new)
local _, success = self:remove_prereq(old)
if success then
@@ -89,10 +79,6 @@ metas.replace_prereq = function(self, old, new)
end
end
----@param self data.TechnologyPrototype
----@param science_pack_name data.ItemID
----@return data.TechnologyPrototype self
----@return boolean success
metas.remove_pack = function(self, science_pack_name)
if not self.unit then
return self, true -- should it be true? false?
@@ -109,15 +95,12 @@ metas.remove_pack = function(self, science_pack_name)
end
-- possible to add the same pack twice, should probably check for that
----@param self data.TechnologyPrototype
----@param science_pack_name data.ItemID
----@return data.TechnologyPrototype self
----@return boolean success
metas.add_pack = function(self, science_pack_name)
if self.research_trigger then
error("WARNING @ \'" .. self.name .. "\':add_pack(): Attempted to add science packs to technology with research_trigger.")
end
+ ---@diagnostic disable-next-line: assign-type-mismatch
self.unit = self.unit or {ingredients = {}}
for _, ingredient in pairs(self.unit.ingredients) do
diff --git a/lib/metas/tile.lua b/lib/metas/tile.lua
index 6a9d935..6827437 100644
--- a/lib/metas/tile.lua
+++ b/lib/metas/tile.lua
@@ -1,6 +1,6 @@
----@class data.TilePrototype
+---@class pYdata.TilePrototype:pYdata.AnyPrototype,data.TilePrototype
+---@operator call(string|pYdata.TilePrototype|data.TilePrototype): pYdata.TilePrototype
TILE = setmetatable(data.raw.tile, {
- ---@param tile data.TilePrototype
__call = function(self, tile)
local ftype = type(tile)
if ftype == "string" then
@@ -16,6 +16,8 @@ TILE = setmetatable(data.raw.tile, {
end
})
+---@diagnostic disable-next-line: missing-fields
+---@type pYdata.TilePrototype
local metas = {}
return metas
diff --git a/lib/pipe-connections.lua b/lib/pipe-connections.lua
index 496cb1c..7ca5e60 100644
--- a/lib/pipe-connections.lua
+++ b/lib/pipe-connections.lua
@@ -12,8 +12,8 @@ py.pipe_pictures = function(pictures, shift_north, shift_south, shift_west, shif
{
filename = "__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-N.png",
priority = "extra-high",
- width = 71,
- height = 38,
+ width = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-N").width,
+ height = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-N").height,
shift = shift_north,
scale = 0.5
} or py.empty_image(),
@@ -21,8 +21,8 @@ py.pipe_pictures = function(pictures, shift_north, shift_south, shift_west, shif
{
filename = "__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-S.png",
priority = "extra-high",
- width = 88,
- height = 61,
+ width = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-S").width,
+ height = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-S").height,
shift = shift_south,
scale = 0.5
} or py.empty_image(),
@@ -30,8 +30,8 @@ py.pipe_pictures = function(pictures, shift_north, shift_south, shift_west, shif
{
filename = "__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-W.png",
priority = "extra-high",
- width = 39,
- height = 73,
+ width = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-W").width,
+ height = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-W").height,
shift = shift_west,
scale = 0.5
} or py.empty_image(),
@@ -39,8 +39,8 @@ py.pipe_pictures = function(pictures, shift_north, shift_south, shift_west, shif
{
filename = "__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-E.png",
priority = "extra-high",
- width = 42,
- height = 76,
+ width = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-E").width,
+ height = require("__base__/graphics/entity/" .. pictures .. "/" .. pictures .. "-pipe-E").height,
shift = shift_east,
scale = 0.5
} or py.empty_image()
@@ -50,6 +50,7 @@ py.pipe_pictures = function(pictures, shift_north, shift_south, shift_west, shif
new_pictures[direction].filename = image.filename
new_pictures[direction].width = image.width
new_pictures[direction].height = image.height
+ new_pictures[direction].size = image.size
new_pictures[direction].priority = image.priority or new_pictures[direction].priority
new_pictures[direction].scale = 1 or new_pictures[direction].scale
end
diff --git a/prototypes/functions/compatibility/reverse-factory.lua b/prototypes/functions/compatibility/reverse-factory.lua
index 72d8161..3a04d00 100644
--- a/prototypes/functions/compatibility/reverse-factory.lua
+++ b/prototypes/functions/compatibility/reverse-factory.lua
@@ -1,11 +1,10 @@
if mods["reverse-factory"] then
- local cat = table.invert {"recycle-products", "recycle-intermediates", "recycle-with-fluids", "recycle-productivity"}
+ local whitelist = {"recycle-products", "recycle-intermediates", "recycle-with-fluids", "recycle-productivity"}
for item_name, item in py.iter_prototypes("item") do
local recipe_name = "rf-" .. item_name
- if data.raw.recipe[recipe_name] and cat[data.raw.recipe[recipe_name].category] then
- data.raw.recipe[recipe_name].unlock_results = false
+ if data.raw.recipe[recipe_name] and RECIPE(recipe_name):has_categories(whitelist) then
if ITEM(item).hidden then
data.raw.recipe[recipe_name].hidden = true
end
@@ -15,8 +14,7 @@ if mods["reverse-factory"] then
for fluid_name, fluid in pairs(data.raw.fluid) do
local recipe_name = "rf-" .. fluid_name
- if data.raw.recipe[recipe_name] and cat[data.raw.recipe[recipe_name].category] then
- data.raw.recipe[recipe_name].unlock_results = false
+ if data.raw.recipe[recipe_name] and RECIPE(recipe_name):has_categories(whitelist) then
if fluid.hidden then
data.raw.recipe[recipe_name].hidden = true
end
diff --git a/prototypes/functions/compatibility/transport-drones.lua b/prototypes/functions/compatibility/transport-drones.lua
index b5989de..2ac5ee5 100644
--- a/prototypes/functions/compatibility/transport-drones.lua
+++ b/prototypes/functions/compatibility/transport-drones.lua
@@ -7,7 +7,7 @@ if mods["Transport_Drones"] then
"fluid-depot"
}
data.raw.technology["transport-system"].prerequisites = nil
- data.raw.recipe["road"].category = "crafting-with-fluid"
+ data.raw.recipe["road"].categories = {"crafting-with-fluid"}
data.raw.recipe["road"].ingredients = data.raw.recipe["concrete"].ingredients
TECHNOLOGY("transport-drone-capacity-1"):add_prereq("logistic-science-pack")
TECHNOLOGY("transport-drone-speed-1"):add_prereq("logistic-science-pack")
diff --git a/prototypes/functions/compatibility/yirailway.lua b/prototypes/functions/compatibility/yirailway.lua
index 812c6ba..20c2261 100644
--- a/prototypes/functions/compatibility/yirailway.lua
+++ b/prototypes/functions/compatibility/yirailway.lua
@@ -4,7 +4,7 @@ if mods["yi_railway"] and mods["pyindustry"] then
if recipe.subgroup and string.sub(recipe.subgroup, 1, 4) == "yir_" then
if recipe.subgroup == "yir_locomotives_steam" then
recipe.enabled = true
- recipe.category = data.raw.recipe["locomotive"].category
+ recipe.categories = data.raw.recipe["locomotive"].categories
recipe.group = data.raw.recipe["locomotive"].group
recipe.energy_required = data.raw.recipe["locomotive"].energy_required
recipe.ingredients = data.raw.recipe["locomotive"].ingredients -- I know what this does and I don't care
@@ -29,7 +29,7 @@ if mods["yi_railway"] and mods["pyindustry"] then
end
elseif recipe.subgroup == "yir_locomotives_diesel" or recipe.subgroup == "yir_locomotives_nslong" then
recipe.enabled = true
- recipe.category = data.raw.recipe["mk02-locomotive"].category
+ recipe.categories = data.raw.recipe["mk02-locomotive"].categories
recipe.group = data.raw.recipe["mk02-locomotive"].group
recipe.energy_required = data.raw.recipe["mk02-locomotive"].energy_required
recipe.ingredients = data.raw.recipe["mk02-locomotive"].ingredients
@@ -52,7 +52,7 @@ if mods["yi_railway"] and mods["pyindustry"] then
elseif recipe.subgroup == "yir_cargowagons" or recipe.subgroup == "yir_cargowagons_4A" or
recipe.subgroup == "yir_cargowagons_2A2" then
recipe.enabled = true
- recipe.category = data.raw.recipe["cargo-wagon"].category
+ recipe.categories = data.raw.recipe["cargo-wagon"].categories
recipe.group = data.raw.recipe["cargo-wagon"].group
recipe.energy_required = data.raw.recipe["cargo-wagon"].energy_required
recipe.ingredients = data.raw.recipe["cargo-wagon"].ingredients
@@ -71,7 +71,7 @@ if mods["yi_railway"] and mods["pyindustry"] then
ywagon.inventory_size = wagon.inventory_size
elseif recipe.subgroup == "yir_tankwagons2a" and recipe.subgroup == "yir_fluidwagons_4A" then
recipe.enabled = true
- recipe.category = data.raw.recipe["fluid-wagon"].category
+ recipe.categories = data.raw.recipe["fluid-wagon"].categories
recipe.group = data.raw.recipe["fluid-wagon"].group
recipe.energy_required = data.raw.recipe["fluid-wagon"].energy_required
recipe.ingredients = data.raw.recipe["fluid-wagon"].ingredients
@@ -89,7 +89,6 @@ if mods["yi_railway"] and mods["pyindustry"] then
ywagon.air_resistance = wagon.air_resistance
ywagon.capacity = wagon.capacity
else
- recipe.unlock_results = false
recipe.hidden = true
if (recipe.results) then
for i, v in ipairs(recipe.results[1]) do
@@ -99,7 +98,6 @@ if mods["yi_railway"] and mods["pyindustry"] then
end
end
elseif string.sub(recipe_name, 1, 4) == "yir_" and string.find(recipe_name, "pyvoid") == nil then
- recipe.unlock_results = false
recipe.hidden = true
if (recipe.results) then
for i, v in ipairs(recipe.results[1]) do
diff --git a/prototypes/yafc.lua b/prototypes/yafc.lua
index 726a4a4..e429011 100644
--- a/prototypes/yafc.lua
+++ b/prototypes/yafc.lua
@@ -161,7 +161,7 @@ if mods["pyalienlife"] then
{type = "item", name = y[1], amount = 4}
},
main_product = "nexelit-ore",
- category = "dino-dig-site"
+ categories = {"dino-dig-site"}
}
end
end
@@ -188,7 +188,7 @@ if mods["pyalienlife"] then
{type = "item", name = creature_name, amount = 1}
},
main_product = "guano",
- category = "biofluid"
+ categories = {"biofluid"}
}
end
end
diff --git a/tests/control.lua b/tests/control.lua
index 8a0459d..4b76083 100644
--- a/tests/control.lua
+++ b/tests/control.lua
@@ -1,9 +1,8 @@
local total_string_count
local function test_localised_strings()
- local excluded_categories = {}
local localised_strings = {}
for _, recipe in pairs(prototypes.recipe) do
- if not excluded_categories[recipe.category] and not recipe.hidden then table.insert(localised_strings, recipe.localised_name) end
+ if not recipe.hidden then table.insert(localised_strings, recipe.localised_name) end
end
local excluded_types = {}
for _, category in pairs {"item", "fluid", "entity"} do
diff --git a/tests/data.lua b/tests/data.lua
index 09c97d2..380e602 100644
--- a/tests/data.lua
+++ b/tests/data.lua
@@ -23,7 +23,7 @@ local function test_entity_graphics()
["electric-energy-interface"] = {{"picture", "pictures", "animation", "animations"}},
["electric-pole"] = {"pictures"},
["fish"] = {"pictures"},
- ["generator"] = {"horizontal_animation", "vertical_animation"},
+ ["generator"] = {"pictures"},
["heat-interface"] = {"picture"},
["heat-pipe"] = {"connection_sprites", "heat_glow_sprites"},
["inserter"] = {"hand_base_picture", "hand_closed_picture", "hand_open_picture", "hand_base_shadow", "hand_closed_shadow", "hand_open_shadow"},
@@ -54,6 +54,7 @@ local function test_entity_graphics()
["solar-panel"] = {"picture"},
["storage-tank"] = {"pictures"},
["tree"] = {{"pictures", "variations"}},
+ ["valve"] = {"animations"},
["wall"] = {"pictures"},
}
local excluded_types = {
@@ -224,7 +225,6 @@ local function factoriopedia_recipes(check_absent_recipes)
["space-science-pack"] = 12,
}
local unlocks = {}
- local barreling = {["py-barreling"] = true, ["py-unbarreling"] = true}
for _, tech in pairs(data.raw["technology"]) do
local unit_tech = table.deepcopy(tech)
local science
@@ -255,7 +255,7 @@ local function factoriopedia_recipes(check_absent_recipes)
end
end
for _, modifier in pairs(tech.effects or {}) do
- if modifier.type == "unlock-recipe" and not barreling[data.raw["recipe"][modifier.recipe].category] then
+ if modifier.type == "unlock-recipe" and not RECIPE(modifier.recipe):has_categories{"py-barreling", "py-unbarreling"} then
if not science then
for _, pack in pairs(unit_tech.unit.ingredients) do
if pack[2] == 1 then
@@ -271,7 +271,7 @@ local function factoriopedia_recipes(check_absent_recipes)
end
end
for name, recipe in pairs(data.raw["recipe"]) do
- if recipe.enabled ~= false and not recipe.category == "py-incineration" and not recipe.hidden then
+ if recipe.enabled ~= false and not RECIPE(name):has_category("py-incineration") and not recipe.hidden then
for _, product in pairs(recipe.results) do
unlocks[product.name] = unlocks[product.name] or {}
table.insert(unlocks[product.name], {science = 0, recipe = name})
diff --git a/tests/scenario-tests.lua b/tests/scenario-tests.lua
index 17f85f2..598f144 100644
--- a/tests/scenario-tests.lua
+++ b/tests/scenario-tests.lua
@@ -1,92 +1,93 @@
-local tests = {}
-
-local helper = require("scenario-helper")
-
-local turd_upgrade_names_table = {
- "prototypes/upgrades/biofactory",
- "prototypes/upgrades/compost",
- "prototypes/upgrades/creature",
- "prototypes/upgrades/incubator",
- "prototypes/upgrades/slaughterhouse",
- "prototypes/upgrades/arthurian",
- "prototypes/upgrades/dhilmos",
- "prototypes/upgrades/dingrits",
- "prototypes/upgrades/korlex",
- "prototypes/upgrades/fawogae",
- "prototypes/upgrades/moss",
- "prototypes/upgrades/scrondrix",
- "prototypes/upgrades/vonix",
- "prototypes/upgrades/yaedols",
- "prototypes/upgrades/fwf",
- "prototypes/upgrades/cadaveric",
- "prototypes/upgrades/moondrop",
- "prototypes/upgrades/auog",
- "prototypes/upgrades/arqad",
- "prototypes/upgrades/phadai",
- "prototypes/upgrades/phagnot",
- "prototypes/upgrades/sponge",
- "prototypes/upgrades/tuuphra",
- "prototypes/upgrades/ulric",
- "prototypes/upgrades/vrauks",
- "prototypes/upgrades/xyhiphoe",
- "prototypes/upgrades/seaweed",
- "prototypes/upgrades/atomizer",
- "prototypes/upgrades/bioreactor",
- "prototypes/upgrades/zungror",
- "prototypes/upgrades/numal",
- "prototypes/upgrades/data-array",
- "prototypes/upgrades/xeno",
- "prototypes/upgrades/fish",
- "prototypes/upgrades/cottongut",
- "prototypes/upgrades/guar",
- "prototypes/upgrades/kicalk",
- "prototypes/upgrades/rennea",
- "prototypes/upgrades/navens",
- "prototypes/upgrades/antelope",
- "prototypes/upgrades/bhoddos",
- "prototypes/upgrades/genlab",
- "prototypes/upgrades/grod",
- "prototypes/upgrades/research",
- "prototypes/upgrades/yotoi",
- "prototypes/upgrades/cridren",
- "prototypes/upgrades/kmauts",
- "prototypes/upgrades/trits",
- "prototypes/upgrades/ralesia",
- "prototypes/upgrades/mukmoux",
- "prototypes/upgrades/simikmetalMK01",
- "prototypes/upgrades/simikmetalMK02",
- "prototypes/upgrades/simikmetalMK03",
- "prototypes/upgrades/simikmetalMK04",
- "prototypes/upgrades/simikmetalMK05",
- "prototypes/upgrades/simikmetalMK06",
- "prototypes/upgrades/sap",
- "prototypes/upgrades/bioprinting",
- "prototypes/upgrades/zipir",
- "prototypes/upgrades/wpu",
-}
-local turds = {}
-for i, t in pairs(turd_upgrade_names_table) do
- turds[i] = require("__pyalienlife__/" .. t)
-end
-
-
-function tests.auog_turd_crash()
- helper.select_turd("auog-upgrade", "glowing-mushrooms")
- local auog_power_gen = game.surfaces.nauvis.create_entity{name = "generator-1", position = {0, 0}, force = game.player.force, player = game.player, raise_built = true}
- helper.select_turd("auog-upgrade", "glowing-mushrooms")
- helper.get_entity_at({0, 0}).destroy()
-
- return "Auog Glowing Mushrooms Turd"
-end
-
-function tests.test_all_turds()
- for _,turd in pairs(turds) do
- for _,subtech in pairs(turd.sub_techs) do
- helper.select_turd(turd.master_tech.name, subtech.name)
- helper.select_turd(turd.master_tech.name, subtech.name)
- end
- end
- return "Test all turds"
-end
-
-return tests
+if not script.active_mods.pyalienlife then return end
+local tests = {}
+
+local helper = require("scenario-helper")
+
+local turd_upgrade_names_table = {
+ "prototypes/upgrades/biofactory",
+ "prototypes/upgrades/compost",
+ "prototypes/upgrades/creature",
+ "prototypes/upgrades/incubator",
+ "prototypes/upgrades/slaughterhouse",
+ "prototypes/upgrades/arthurian",
+ "prototypes/upgrades/dhilmos",
+ "prototypes/upgrades/dingrits",
+ "prototypes/upgrades/korlex",
+ "prototypes/upgrades/fawogae",
+ "prototypes/upgrades/moss",
+ "prototypes/upgrades/scrondrix",
+ "prototypes/upgrades/vonix",
+ "prototypes/upgrades/yaedols",
+ "prototypes/upgrades/fwf",
+ "prototypes/upgrades/cadaveric",
+ "prototypes/upgrades/moondrop",
+ "prototypes/upgrades/auog",
+ "prototypes/upgrades/arqad",
+ "prototypes/upgrades/phadai",
+ "prototypes/upgrades/phagnot",
+ "prototypes/upgrades/sponge",
+ "prototypes/upgrades/tuuphra",
+ "prototypes/upgrades/ulric",
+ "prototypes/upgrades/vrauks",
+ "prototypes/upgrades/xyhiphoe",
+ "prototypes/upgrades/seaweed",
+ "prototypes/upgrades/atomizer",
+ "prototypes/upgrades/bioreactor",
+ "prototypes/upgrades/zungror",
+ "prototypes/upgrades/numal",
+ "prototypes/upgrades/data-array",
+ "prototypes/upgrades/xeno",
+ "prototypes/upgrades/fish",
+ "prototypes/upgrades/cottongut",
+ "prototypes/upgrades/guar",
+ "prototypes/upgrades/kicalk",
+ "prototypes/upgrades/rennea",
+ "prototypes/upgrades/navens",
+ "prototypes/upgrades/antelope",
+ "prototypes/upgrades/bhoddos",
+ "prototypes/upgrades/genlab",
+ "prototypes/upgrades/grod",
+ "prototypes/upgrades/research",
+ "prototypes/upgrades/yotoi",
+ "prototypes/upgrades/cridren",
+ "prototypes/upgrades/kmauts",
+ "prototypes/upgrades/trits",
+ "prototypes/upgrades/ralesia",
+ "prototypes/upgrades/mukmoux",
+ "prototypes/upgrades/simikmetalMK01",
+ "prototypes/upgrades/simikmetalMK02",
+ "prototypes/upgrades/simikmetalMK03",
+ "prototypes/upgrades/simikmetalMK04",
+ "prototypes/upgrades/simikmetalMK05",
+ "prototypes/upgrades/simikmetalMK06",
+ "prototypes/upgrades/sap",
+ "prototypes/upgrades/bioprinting",
+ "prototypes/upgrades/zipir",
+ "prototypes/upgrades/wpu",
+}
+local turds = {}
+for i, t in pairs(turd_upgrade_names_table) do
+ turds[i] = require("__pyalienlife__/" .. t)
+end
+
+
+function tests.auog_turd_crash()
+ helper.select_turd("auog-upgrade", "glowing-mushrooms")
+ local auog_power_gen = game.surfaces.nauvis.create_entity{name = "generator-1", position = {0, 0}, force = game.player.force, player = game.player, raise_built = true}
+ helper.select_turd("auog-upgrade", "glowing-mushrooms")
+ helper.get_entity_at({0, 0}).destroy()
+
+ return "Auog Glowing Mushrooms Turd"
+end
+
+function tests.test_all_turds()
+ for _,turd in pairs(turds) do
+ for _,subtech in pairs(turd.sub_techs) do
+ helper.select_turd(turd.master_tech.name, subtech.name)
+ helper.select_turd(turd.master_tech.name, subtech.name)
+ end
+ end
+ return "Test all turds"
+end
+
+return tests
From f65a35eba78a755e51a1b80d9e93ad03da4f88c6 Mon Sep 17 00:00:00 2001
From: oorzkws <65210810+oorzkws@users.noreply.github.com>
Date: Fri, 26 Jun 2026 13:18:40 -0600
Subject: [PATCH 2/5] Fix defines.inventory spec for module sizing
---
prototypes/fancy-module-slots.lua | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/prototypes/fancy-module-slots.lua b/prototypes/fancy-module-slots.lua
index 6aab672..629faaf 100644
--- a/prototypes/fancy-module-slots.lua
+++ b/prototypes/fancy-module-slots.lua
@@ -4,10 +4,10 @@ if mods["omnimatter_compression"] then return end
-- custom module alt-mode draw positioning
for prototype_name, inventory in pairs {
["mining-drill"] = defines.inventory.mining_drill_modules,
- ["assembling-machine"] = defines.inventory.assembling_machine_modules,
- ["furnace"] = defines.inventory.furnace_modules,
+ ["assembling-machine"] = defines.inventory.crafter_modules,
+ ["furnace"] = defines.inventory.crafter_modules,
["lab"] = defines.inventory.lab_modules,
- ["rocket-silo"] = defines.inventory.rocket_silo_modules,
+ ["rocket-silo"] = defines.inventory.crafter_modules,
["beacon"] = defines.inventory.beacon_modules,
} do
for _, machine in pairs(data.raw[prototype_name] or {}) do
From 12cb8f6ac9c382da8eb5684973166dc7bb2b1d38 Mon Sep 17 00:00:00 2001
From: protocol_1903 <67478786+protocol-1903@users.noreply.github.com>
Date: Sun, 28 Jun 2026 23:21:36 -0700
Subject: [PATCH 3/5] unhide the shortcut to toggle tall entity visibility
---
data-final-fixes.lua | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/data-final-fixes.lua b/data-final-fixes.lua
index af10eb6..450155d 100644
--- a/data-final-fixes.lua
+++ b/data-final-fixes.lua
@@ -530,6 +530,10 @@ for _, bot_type in pairs {"construction-robot", "logistic-robot"} do
end
end
+-- unhide the shortcut to toggle tall entities and make it available from the start
+data.raw.shortcut["toggle-tall-entity-visibility"].hidden = nil
+data.raw.shortcut["toggle-tall-entity-visibility"].technology_to_unlock = nil
+
-- Skip check if user has [declutter](https://mods.factorio.com/mod/declutter) mod which hides arbitrary techs
-- Also skip check if user has autotech mod, since autotech runs after this check.
if not (mods.declutter or mods.autotech) then
From b241428b220ef155334c3501f2a29ff208752824 Mon Sep 17 00:00:00 2001
From: protocol_1903 <67478786+protocol-1903@users.noreply.github.com>
Date: Thu, 2 Jul 2026 02:26:24 -0700
Subject: [PATCH 4/5] Added py.generate_alert and py.clear_alert to generate
rendered and pinnable alerts for all players on a force
---
changelog.txt | 1 +
lib/alerts.lua | 90 +++++++++++++++++++++++++++++++++++++++++++
lib/control-stage.lua | 20 +---------
3 files changed, 92 insertions(+), 19 deletions(-)
create mode 100644 lib/alerts.lua
diff --git a/changelog.txt b/changelog.txt
index 561320e..edac17b 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -10,6 +10,7 @@ Date: ???
- Updated global checker
- Updated py.pipe_pictures to load graphics definitions directly from base instead of inferring them, allowing more to be used
- Fixed a potential crash when no tools are defined
+ - Added py.generate_alert and py.clear_alert to generate rendered and pinnable alerts for all players on a force
---------------------------------------------------------------------------------------------------
Version: 3.0.42
Date: 2026-05-27
diff --git a/lib/alerts.lua b/lib/alerts.lua
new file mode 100644
index 0000000..63e44fa
--- /dev/null
+++ b/lib/alerts.lua
@@ -0,0 +1,90 @@
+-- functions and helpers for alerts, warnings, and errors
+
+---Draws a red error icon at the entity's position.
+---@param entity LuaEntity
+---@param sprite string
+---@param time_to_live integer? default forever
+---@param blink_interval integer? default 30 ticks
+---@return LuaRenderObject
+py.draw_error_sprite = function(entity, sprite, time_to_live, blink_interval)
+ return rendering.draw_sprite {
+ sprite = sprite,
+ x_scale = entity.prototype.alert_icon_scale or 0.5,
+ y_scale = entity.prototype.alert_icon_scale or 0.5,
+ target = entity,
+ surface = entity.surface,
+ time_to_live = time_to_live,
+ blink_interval = blink_interval or 30,
+ render_layer = "air-entity-info-icon"
+ }
+end
+
+---Generates an error icon and alert at the entity's position, refreshing both until cancelled
+---@param entity LuaEntity entity alert is tied to
+---@param signal SignalID sprite of generated alert
+---@param icon SpritePath sprite of rendered alert
+---@param message LocalisedString message of alert
+---@param show_on_map boolean whether to show alert on map
+---@return uint alert_id unique identifier of this alert
+py.generate_alert = function(entity, signal, icon, message, show_on_map)
+ if not entity or not entity.valid or not signal then return end
+ storage.alert_count = storage.alert_count + 1
+ storage.alerts[storage.alert_count] = {
+ entity = entity,
+ surface = entity.surface,
+ force = entity.force,
+ signal = signal,
+ message = message,
+ show_on_map = show_on_map,
+ offset = game.tick % 600,
+ rendering = py.draw_error_sprite(entity, icon)
+ }
+ return storage.alert_count
+end
+
+---Clears an alert from a force. Works if referenced entity is invalid
+---@param alert_id uint
+py.clear_alert = function(alert_id)
+ if not alert_id then return end
+ local alert_data = storage.alerts[alert_id]
+ if alert_data.entity and alert_data.entity.valid then
+ alert_data.entity.force.remove_alert{
+ entity = alert_data.entity,
+ surface = alert_data.surface
+ }
+ else
+ for _, alert in pairs(alert_data.force.players[1].get_alerts{
+ type = defines.alert_type.custom,
+ surface = alert_data.surface,
+ icon = alert_data.signal,
+ message = alert_data.message
+ }) do
+ alert_data.force.remove_alert(alert)
+ end
+ end
+ storage.alerts[alert_id] = nil
+end
+
+py.on_event(defines.events.on_tick, function()
+ -- check stored alerts, update if required
+ local offset = game.tick % 600
+ for i, alert_data in pairs(storage.alerts or {}) do
+ if alert_data.offset == offset then
+ if not alert_data.entity.valid then
+ storage.alerts[i] = nil
+ else
+ alert_data.entity.force.add_custom_alert(
+ alert_data.entity,
+ alert_data.signal,
+ alert_data.message,
+ alert_data.show_on_map
+ )
+ end
+ end
+ end
+end)
+
+py.on_event(py.events.on_init(), function()
+ storage.alerts = storage.alerts or {}
+ storage.alert_count = storage.alert_count or 0
+end)
\ No newline at end of file
diff --git a/lib/control-stage.lua b/lib/control-stage.lua
index dfbb664..aa01207 100644
--- a/lib/control-stage.lua
+++ b/lib/control-stage.lua
@@ -7,25 +7,7 @@ require "vector"
require "smuggler"
require "compound-entities"
require "inventory"
-
----Draws a red error icon at the entity's position.
----@param entity LuaEntity
----@param sprite string
----@param time_to_live integer? default forever
----@param blink_interval integer? default 30 ticks
----@return LuaRenderObject
-py.draw_error_sprite = function(entity, sprite, time_to_live, blink_interval)
- return rendering.draw_sprite {
- sprite = sprite,
- x_scale = entity.prototype.alert_icon_scale or 0.5,
- y_scale = entity.prototype.alert_icon_scale or 0.5,
- target = entity,
- surface = entity.surface,
- time_to_live = time_to_live,
- blink_interval = blink_interval or 30,
- render_layer = "air-entity-info-icon"
- }
-end
+require "alerts"
---Creates a localised string tooltip for allowed modules.
---@param allowed_modules table
From 53f2badf4f675dfe6b97f676c0ebc27d51a9a491 Mon Sep 17 00:00:00 2001
From: protocol_1903 <67478786+protocol-1903@users.noreply.github.com>
Date: Thu, 2 Jul 2026 02:41:23 -0700
Subject: [PATCH 5/5] also clear rendering when cleared
---
lib/alerts.lua | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/alerts.lua b/lib/alerts.lua
index 63e44fa..83a6d9e 100644
--- a/lib/alerts.lua
+++ b/lib/alerts.lua
@@ -47,11 +47,13 @@ end
py.clear_alert = function(alert_id)
if not alert_id then return end
local alert_data = storage.alerts[alert_id]
+ if not alert_data then return end
if alert_data.entity and alert_data.entity.valid then
alert_data.entity.force.remove_alert{
entity = alert_data.entity,
surface = alert_data.surface
}
+ alert_data.rendering.destroy()
else
for _, alert in pairs(alert_data.force.players[1].get_alerts{
type = defines.alert_type.custom,