Wed, 23 Jul 2025 14:46:54 +0300
Update to Factorio 2.0.60
---@type data.FeatureFlags feature_flags = feature_flags local function seconds(x) return {"time-symbol-seconds", tostring(x)} end local energy_zero_table = { ["0W"] = 1, ["0kW"] = 1, ["0MW"] = 1, ["0GW"] = 1, ["0TW"] = 1, } ---@type table<string, 1> local recipes_that_have_productivity_research = {} for _, technology in pairs(data.raw["technology"] or {}) do for _, effect in pairs(technology.effects or {}) do if effect.type == "change-recipe-productivity" then recipes_that_have_productivity_research[effect.recipe] = 1 end end end ---@param x string local function energy_zero(x) return x == nil or energy_zero_table[x] end ---@param x LocalisedString -- localised string containing too many entries? snap it into many pieces and catenate them local function snap(x) if type(x) == "table" and #x > 20 and x[1] == "" then local max_per_part = math.ceil((#x - 1) / 18) local result = {""} local segment = {""} for k, v in pairs(x) do if k > 1 then if #segment >= max_per_part + 1 then table.insert(result, snap(segment)) segment = {""} end table.insert(segment, v) end end if #segment > 1 then table.insert(result, snap(segment)) end return result else return x end end local item_categories = { "item", "item-with-entity-data", "rail-planner", "capsule", "repair-tool", "blueprint", "deconstruction-item", "upgrade-item", "blueprint-book", "copy-paste-tool", "module", "tool", "gun", "ammo", "space-platform-starter-pack", "armor", "spidertron-remote", } local entity_categories = { "container", "storage-tank", "transport-belt", "underground-belt", "splitter", "loader", "inserter", "electric-pole", "pipe", "pipe-to-ground", "pump", "straight-rail", "half-diagonal-rail", "curved-rail-a", "curved-rail-b", "elevated-straight-rail", "elevated-half-diagonal-rail", "elevated-curved-rail-a", "elevated-curved-rail-b", "legacy-straight-rail", "legacy-curved-rail", "rail-ramp", "rail-support", "train-stop", "rail-signal", "rail-chain-signal", "locomotive", "cargo-wagon", "fluid-wagon", "artillery-wagon", "car", "spider-vehicle", "logistic-robot", "construction-robot", "logistic-container", "roboport", "lamp", "arithmetic-combinator", "decider-combinator", "selector-combinator", "constant-combinator", "power-switch", "programmable-speaker", "display-panel", "boiler", "generator", "fusion-reactor", "fusion-generator", "mining-drill", "offshore-pump", "furnace", "assembling-machine", "agricultural-tower", "lab", "lightning-attractor", "reactor", "beacon", "rocket-silo", "cargo-landing-pad", "space-platform-hub", "cargo-bay", "asteroid-collector", "thruster", "wall", "gate", "radar", "land-mine", "ammo-turret", "electric-turret", "fluid-turret", "artillery-turret", "plant", "simple-entity-with-force", "simple-entity-with-owner", "electric-energy-interface", "linked-container", "proxy-container", "heat-interface", "heat-pipe", "lane-splitter", "linked-belt", "valve", "infinity-cargo-wagon", "infinity-container", "infinity-pipe", "burner-generator", "resource", "cargo-pod", "temporary-container", "asteroid", "combat-robot", "unit", "turret", "unit-spawner", "spider-unit", "segmented-unit", "cliff", "character", "fish", "tree", "simple-entity", "lightning", "corpse", "rail-remnants", "explosion", "particle-source", "fire", "sticker", "stream", "artillery-flare", "artillery-projectile", "projectile", "segment", "spider-leg", "beam", "character-corpse", "speech-bubble", "smoke-with-trigger", "entity-ghost", "arrow", "highlight-box", "item-entity", "item-request-proxy", "loader-1x1", "rocket-silo-rocket", "rocket-silo-rocket-shadow", "tile-ghost", "market", "capture-robot", "solar-panel", } -- Find all entity categories that contain prototypes that can freeze. -- If a prototype in such a category cannot freeze, that's worth pointing out. -- However, it's not worth pointing out that the steel chest cannot freeze, -- since no container freezes (in vanilla anyway). local freezable_entity_categories = {} if feature_flags.freezing then for _, category in pairs(entity_categories) do for _, entity in pairs(data.raw[category] or {}) do if not energy_zero(entity.heating_energy) then freezable_entity_categories[entity.type] = 1 break end end end end local function find_entity(name) for _, entity_category in pairs(entity_categories) do if data.raw[entity_category] and data.raw[entity_category][name] then return data.raw[entity_category][name] end end return nil end -- @type table<string, string> local ammo_categories = {} for _, ammo in pairs(data.raw.ammo) do if not ammo_categories[ammo.ammo_category] then ammo_categories[ammo.ammo_category] = "" end if #ammo_categories[ammo.ammo_category] < 120 then ammo_categories[ammo.ammo_category] = ammo_categories[ammo.ammo_category].."[item="..ammo.name.."]" elseif string.sub(ammo_categories[ammo.ammo_category], -3, -1) ~= "..." then ammo_categories[ammo.ammo_category] = ammo_categories[ammo.ammo_category] .. "..." end end ---@param proto data.Prototype ---@param tooltip_field LocalisedString local function generic_add_description(proto, tooltip_field) if not proto.custom_tooltip_fields then proto.custom_tooltip_fields = {} end table.insert(proto.custom_tooltip_fields, tooltip_field) end for _, item_type in pairs(item_categories) do for _, item in pairs(data.raw[item_type] or {}) do local recycling_recipe = data.raw.recipe[item.name.."-recycling"] ---@param tooltip_field data.CustomTooltipField local function add_description(tooltip_field) generic_add_description(item, tooltip_field) end local add_ammo_from_attack_parameters = function(attack_parameters) if attack_parameters.ammo_category and ammo_categories[attack_parameters.ammo_category] then add_description{ name = {"more-descriptions-mod.gun-accepts-ammo"}, value = ammo_categories[attack_parameters.ammo_category]} end for _, category in pairs(attack_parameters.ammo_categories or {}) do if ammo_categories[category] then add_description{ name = {"more-descriptions-mod.gun-accepts-ammo"}, value = ammo_categories[category]} end end end if recycling_recipe ~= nil then local recycling_results = {""} for _, result in pairs(recycling_recipe.results) do table.insert(recycling_results, "[img="..result.type.."."..result.name.."]") end add_description{ name = {"more-descriptions-mod.recycling"}, value = { "more-descriptions-mod.recycling-results", seconds(recycling_recipe.energy_required), snap(recycling_results), }, } end if mods["promethium-quality"] then local refining_recipe = data.raw.recipe[item.name.."-refining"] if refining_recipe ~= nil then add_description{ name = {"more-descriptions-mod.refining-cost"}, value = seconds(refining_recipe.energy_required), } elseif recycling_recipe ~= nil then add_description{ name = {"more-descriptions-mod.cannot-be-refined"}, value = ""} end end if item.type == "ammo" and (item.reload_time or 0) > 0 then add_description{ name = {"more-descriptions-mod.reload-time"}, value = seconds(tostring(item.reload_time / 60.0)), } end if item.type == "active-defense-equipment" and item.automatic then add_description{ name = {"more-descriptions-mod.fires-automatically"}, value = "", } end if item.type == "armor" and item.provides_flight then add_description{ name = {"more-descriptions-mod.armor-provides-flight"}, value = "", } end if item.type == "gun" then add_ammo_from_attack_parameters(item.attack_parameters) end local entity = item.place_result and find_entity(item.place_result) or nil if entity ~= nil then if entity.collision_box then local cb = entity.collision_box local width = math.ceil(cb[2][1] - cb[1][1]) local height = math.ceil(cb[2][2] - cb[1][2]) add_description{ name = {"more-descriptions-mod.entity-size"}, value = { "more-descriptions-mod.size", tostring(width), tostring(height), }, } end if entity.drops_full_belt_stacks then add_description{ name = {"more-descriptions-mod.drops-full-belt-stacks"}, value = ""} end if entity.heat_buffer and entity.heat_buffer.specific_heat then add_description{ name = {"more-descriptions-mod.specific-heat"}, value = { "more-descriptions-mod.energy-per-degrees-celsius", entity.heat_buffer.specific_heat, }, } elseif entity.energy_source and entity.energy_source.type == "heat" and entity.energy_source.specific_heat then add_description{ name = {"more-descriptions-mod.specific-heat"}, value = entity.energy_source.specific_heat, } end if entity.is_military_target then add_description{ name = {"more-descriptions-mod.is-military-target"}, value = "", } end if not energy_zero(entity.heating_energy or "0W") then local value = entity.heating_energy if entity.type == "underground-belt" or entity.type == "pipe-to-ground" then value = {"more-descriptions-mod.value-per-end", value} end add_description{ name = {"more-descriptions-mod.heating-energy"}, value = value, } elseif freezable_entity_categories[entity.type] then add_description{ name = {"more-descriptions-mod.no-heating-energy"}, value = "", } end if entity.type == "agricultural-tower" then local cb = entity.collision_box local W = math.ceil(cb[2][1] - cb[1][1]) local w = (entity.growth_grid_tile_size or 3) -- width of the "buffer" area around the agricultural tower local z = (2 * w * math.ceil((W - w) / 2 / w)) + w -- num of growth cells extending from the edges of the tower local r = math.floor(entity.radius) -- why is it double..? add_description{ name = {"more-descriptions-mod.agricultural-tower-num-inputs"}, value = tostring(entity.input_inventory_size) } add_description{ name = {"more-descriptions-mod.agricultural-tower-growth-cell-size"}, value = { "more-descriptions-mod.size", tostring(w), tostring(w), } } add_description{ name = {"more-descriptions-mod.agricultural-tower-growth-cell-count"}, value = tostring(4 * r * (r + (z / w))) } total_size = tostring(z + 2 * r * w) add_description{ name = {"more-descriptions-mod.agricultural-tower-total-size"}, value = { "more-descriptions-mod.size", total_size, total_size, }, } elseif entity.type == "ammo-turret" then if entity.energy_per_shot ~= nil then add_description{ name = {"more-descriptions-mod.energy-per-shot-fired"}, value = entity.energy_per_shot} end add_ammo_from_attack_parameters(entity.attack_parameters) elseif entity.type == "beacon" then add_description{ name = {"more-descriptions-mod.beacon-supply-area-distance"}, value = tostring(entity.supply_area_distance) } elseif entity.type == "car" then local immunities = "" if (entity.immune_to_tree_impacts or false) then immunities = immunities.."[entity=tree-01]" end if (entity.immune_to_rock_impacts or false) then immunities = immunities.."[entity=big-rock]" end if (entity.immune_to_cliff_impacts or true) then immunities = immunities.."[entity=cliff]" end if immunities ~= "" then add_description{ name = {"more-descriptions-mod.car-immune-to-impacts"},value = immunities} end elseif entity.type == "constant-combinator" then -- used by pushbutton mod if (entity.pulse_duration or 0) > 60 then add_description{ name = {"more-descriptions-mod.constant-combinator-pulse-duration"}, value = seconds(entity.pulse_duration / 60.0), } elseif (entity.pulse_duration or 0) > 0 then add_description{ name = {"more-descriptions-mod.constant-combinator-pulse-duration"}, value = { "more-descriptions-mod.ticks", tostring(entity.pulse_duration), }, } end elseif (entity.type == "container" or entity.type == "logistic-container") then if entity.inventory_type == "with_filters_and_bar" then add_description{ name = {"more-descriptions-mod.container-filters"}, value = "", } end elseif entity.type == "cargo-wagon" then -- all cargo wagons support filters add_description{ name = {"more-descriptions-mod.container-filters"}, value = "", } elseif entity.type == "display-panel" then add_description{ name = {"more-descriptions-mod.display-panel-max-text-width"}, value = tostring(entity.max_text_width or 400), } elseif entity.type == "logistic-robot" or entity.type == "construction-robot" then if entity.speed_multiplier_when_out_of_energy > 0 then add_description{ name = {"more-descriptions-mod.robot-speed-multiplier-when-out-of-energy"}, value = tostring(entity.speed_multiplier_when_out_of_energy * 100), } else add_description{ name = {"more-descriptions-mod.robot-crashes-when-out-of-energy"}, value = "", } end elseif entity.type == "inserter" then if entity.bulk then add_description{ name = {"more-descriptions-mod.inserter-bulk"}, value = tostring(entity.filter_count), } end if entity.wait_for_full_hand then add_description{ name = {"more-descriptions-mod.inserter-wait-for-full-hand"}, value = "", } end if entity.enter_drop_mode_if_held_stack_spoiled then add_description{ name = {"more-descriptions-mod.inserter-enters-drop-mode-if-held-stack-spoils"}, value = "", } end elseif entity.type == "land-mine" then add_description{ name = {"more-descriptions-mod.land-mine-timeout"}, value = seconds((entity.timeout or 120) / 60.0), } elseif entity.type == "radar" then if entity.connects_to_other_radars ~= false then add_description{ name = {"more-descriptions-mod.radar-connection"}, value = "", } end end if entity.filter_count then add_description{ name = {"more-descriptions-mod.filter-count"}, value = tostring(entity.filter_count), } end for _, flag in pairs(entity.flags or {}) do if flag == "no-automated-item-insertion" then add_description{ name = {"more-descriptions-mod.no-automated-item-insertion"}, value = "", } end end end end end for _, recipe in pairs(data.raw.recipe) do ---@param tooltip_field data.CustomTooltipField local function add_description(tooltip_field) generic_add_description(recipe, tooltip_field) end if recipe.allow_productivity then add_description{ name = {"more-descriptions-mod.allows-productivity"}, value = "", } end if recipes_that_have_productivity_research[recipe.name] then add_description{ name = {"more-descriptions-mod.recipe-has-productivity-research"}, value = "", } end if recipe.result_is_always_fresh then add_description{ name = {"more-descriptions-mod.recipe-result-is-always-fresh"}, value = "", } end if recipe.reset_freshness_on_craft then add_description{ name = {"more-descriptions-mod.recipe-result-freshness-reset"}, value = "", } end end -- Stuff mostly specific for entities that you don't place with items (like biters) for _, entity_category in pairs(entity_categories) do for _, entity in pairs(data.raw[entity_category] or {}) do ---@param tooltip_field data.CustomTooltipField local function add_description(tooltip_field) generic_add_description(entity, tooltip_field) end if entity.minable then add_description{ name = {"more-descriptions-mod.mining-time"}, value = seconds(entity.minable.mining_time), } end for _, flag in pairs(entity.flags or {}) do if flag == "breaths-air" then add_description{ name = {"more-descriptions-mod.breathes-air"}, value = "", } end end if entity.type == "unit-spawner" then if (entity.time_to_capture or 0) > 0 then add_description{ name = {"more-descriptions-mod.unit-spawner-time-to-capture"}, value = seconds(entity.time_to_capture / 60.0), } end end end end if feature_flags.space_travel then for _, category in pairs{"space-location", "planet"} do for _, space_location in pairs(data.raw[category]) do ---@param tooltip_field data.CustomTooltipField local function add_description(tooltip_field) generic_add_description(space_location, tooltip_field) end ---@cast space_location data.SpaceLocationPrototype local new_descriptions = {} if (space_location.fly_condition or false) then add_description{ name = {"more-descriptions-mod.space-location-fly-condition"}, value = "", } end if space_location.auto_save_on_first_trip == false -- (nil=true) then add_description{ name = {"more-descriptions-mod.space-location-no-autosave"}, value = "", } end end end end