From 1b75ab432262ba6365b38be3ff6ad300f87c03ee Mon Sep 17 00:00:00 2001 From: RedRafe <93430988+RedRafe@users.noreply.github.com> Date: Fri, 3 Jan 2025 22:34:11 +0100 Subject: [PATCH] Add rocket, inventory & quality utils (#1482) * Add rocket, inventory & quality utils --- .luacheckrc | 1 + features/player_stats.lua | 20 +-- .../april_fools/scenario/rocket_launched.lua | 26 +--- .../april_fools/scenario/rocket_waves.lua | 24 +--- .../exotic_industries/rocket_launched.lua | 22 +--- .../krastorio2/rocket_launched.lua | 22 +--- .../pyanodon/rocket_launched.lua | 22 +--- map_gen/maps/danger_ores/configuration.lua | 1 + .../danger_ores/modules/rocket_launched.lua | 32 ++--- .../modules/rocket_launched_simple.lua | 27 ++-- utils/inventory.lua | 105 ++++++++++++++++ utils/quality.lua | 60 +++++++++ utils/rocket.lua | 115 ++++++++++++++++++ 13 files changed, 319 insertions(+), 158 deletions(-) create mode 100644 utils/inventory.lua create mode 100644 utils/quality.lua create mode 100644 utils/rocket.lua diff --git a/.luacheckrc b/.luacheckrc index 7b5353faa..43a58faca 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1133,6 +1133,7 @@ stds.factorio_defines = { 'on_force_created', 'on_forces_merged', 'on_forces_merging', + 'on_force_reset', 'on_game_created_from_scenario', 'on_gui_checked_state_changed', 'on_gui_click', diff --git a/features/player_stats.lua b/features/player_stats.lua index 2d01fdbbc..9ebb3f199 100644 --- a/features/player_stats.lua +++ b/features/player_stats.lua @@ -1,5 +1,6 @@ local Global = require 'utils.global' local Event = require 'utils.event' +local Rocket = require 'utils.rocket' local SA = require 'utils.space_age' local ScoreTracker = require 'utils.score_tracker' require 'utils.table' @@ -308,24 +309,7 @@ local function rocket_launched(event) change_for_global(rockets_launched_name, 1) - local pod = entity.cargo_pod - if not pod or not pod.valid then - return - end - - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite' }) then return end diff --git a/map_gen/maps/april_fools/scenario/rocket_launched.lua b/map_gen/maps/april_fools/scenario/rocket_launched.lua index 53815086c..004e35e83 100644 --- a/map_gen/maps/april_fools/scenario/rocket_launched.lua +++ b/map_gen/maps/april_fools/scenario/rocket_launched.lua @@ -1,4 +1,5 @@ local Event = require 'utils.event' +local Rocket = require 'utils.rocket' local Server = require 'features.server' local ShareGlobals = require 'map_gen.maps.april_fools.scenario.shared_globals' @@ -55,32 +56,11 @@ local function rocket_launched(event) return end - local pod = entity.cargo_pod - if not pod or not pod.valid then - return - end - - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then - return - end - - local satellite_count = game.forces.player.get_item_launched('satellite') - if satellite_count == 0 then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite' }) then return end + local satellite_count = Rocket.get_item_launched({ name = 'satellite' }) if satellite_count == win_satellite_count then disable_biters() win() diff --git a/map_gen/maps/april_fools/scenario/rocket_waves.lua b/map_gen/maps/april_fools/scenario/rocket_waves.lua index 400797bbf..72eeace5c 100644 --- a/map_gen/maps/april_fools/scenario/rocket_waves.lua +++ b/map_gen/maps/april_fools/scenario/rocket_waves.lua @@ -7,6 +7,7 @@ local Task = require 'utils.task' local Token = require 'utils.token' local Server = require 'features.server' local ShareGlobals = require 'map_gen.maps.april_fools.scenario.shared_globals' +local Rocket = require 'utils.rocket' ShareGlobals.data.map_won = false @@ -134,7 +135,7 @@ local function start_waves(event) Task.set_timeout_in_ticks(1, do_waves, data) - game.print('Warning incomming biter attack! Number of waves: ' .. number_of_waves) + game.print('Warning incoming biter attack! Number of waves: ' .. number_of_waves) end local function rocket_launched(event) @@ -144,28 +145,11 @@ local function rocket_launched(event) return end - local pod = entity.cargo_pod - if not pod or not pod.valid then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite' }) then return end - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then - return - end - - local satellite_count = game.forces.player.get_item_launched('satellite') + local satellite_count = Rocket.get_item_launched({ name = 'satellite' }) if satellite_count == 0 then return end diff --git a/map_gen/maps/danger_ores/compatibility/exotic_industries/rocket_launched.lua b/map_gen/maps/danger_ores/compatibility/exotic_industries/rocket_launched.lua index a372666f6..ea1dcb076 100644 --- a/map_gen/maps/danger_ores/compatibility/exotic_industries/rocket_launched.lua +++ b/map_gen/maps/danger_ores/compatibility/exotic_industries/rocket_launched.lua @@ -1,4 +1,5 @@ local Event = require 'utils.event' +local Rocket = require 'utils.rocket' local RS = require 'map_gen.shared.redmew_surface' local Server = require 'features.server' local ShareGlobals = require 'map_gen.maps.danger_ores.modules.shared_globals' @@ -36,28 +37,11 @@ return function() return end - local pod = entity.cargo_pod - if not pod or not pod.valid then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite' }) then return end - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then - return - end - - local satellite_count = game.forces.player.get_item_launched('satellite') + local satellite_count = Rocket.get_item_launched({ name = 'satellite' }) if satellite_count == 0 then return end diff --git a/map_gen/maps/danger_ores/compatibility/krastorio2/rocket_launched.lua b/map_gen/maps/danger_ores/compatibility/krastorio2/rocket_launched.lua index 3f7f1cd6e..37d712e41 100644 --- a/map_gen/maps/danger_ores/compatibility/krastorio2/rocket_launched.lua +++ b/map_gen/maps/danger_ores/compatibility/krastorio2/rocket_launched.lua @@ -1,5 +1,6 @@ local Event = require 'utils.event' local RS = require 'map_gen.shared.redmew_surface' +local Rocket = require 'utils.rocket' local Server = require 'features.server' local ShareGlobals = require 'map_gen.maps.danger_ores.modules.shared_globals' @@ -36,28 +37,11 @@ return function() return end - local pod = entity.cargo_pod - if not pod or not pod.valid then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite' }) then return end - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then - return - end - - local satellite_count = game.forces.player.get_item_launched('satellite') + local satellite_count = Rocket.get_item_launched({ name = 'satellite' }) if satellite_count == 0 then return end diff --git a/map_gen/maps/danger_ores/compatibility/pyanodon/rocket_launched.lua b/map_gen/maps/danger_ores/compatibility/pyanodon/rocket_launched.lua index 3f089c8cd..5714df0d9 100644 --- a/map_gen/maps/danger_ores/compatibility/pyanodon/rocket_launched.lua +++ b/map_gen/maps/danger_ores/compatibility/pyanodon/rocket_launched.lua @@ -1,4 +1,5 @@ local Event = require 'utils.event' +local Rocket = require 'utils.rocket' local RS = require 'map_gen.shared.redmew_surface' local Server = require 'features.server' local ShareGlobals = require 'map_gen.maps.danger_ores.modules.shared_globals' @@ -36,28 +37,11 @@ return function() return end - local pod = entity.cargo_pod - if not pod or not pod.valid then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite' }) then return end - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then - return - end - - local satellite_count = game.forces.player.get_item_launched('satellite') + local satellite_count = Rocket.get_item_launched({ name = 'satellite' }) if satellite_count == 0 then return end diff --git a/map_gen/maps/danger_ores/configuration.lua b/map_gen/maps/danger_ores/configuration.lua index 3862d49f8..76dacba31 100644 --- a/map_gen/maps/danger_ores/configuration.lua +++ b/map_gen/maps/danger_ores/configuration.lua @@ -58,6 +58,7 @@ return { rocket_launched = { enabled = true, win_satellite_count = 1000, + win_satellite_quality = 'normal', }, terraforming = { enabled = true, diff --git a/map_gen/maps/danger_ores/modules/rocket_launched.lua b/map_gen/maps/danger_ores/modules/rocket_launched.lua index 6f8ea5050..6326fd446 100644 --- a/map_gen/maps/danger_ores/modules/rocket_launched.lua +++ b/map_gen/maps/danger_ores/modules/rocket_launched.lua @@ -3,6 +3,7 @@ local Generate = require 'map_gen.shared.generate' local Global = require 'utils.global' local Queue = require 'utils.queue' local AlienEvolutionProgress = require 'utils.alien_evolution_progress' +local Rocket = require 'utils.rocket' local RS = require 'map_gen.shared.redmew_surface' local Task = require 'utils.task' local Token = require 'utils.token' @@ -18,7 +19,8 @@ return function(config) local win_data = { evolution_rocket_maxed = -1, - extra_rockets = config.extra_rockets or 100 + extra_rockets = config.extra_rockets or 100, + extra_rockets_quality = config.extra_rockets_quality or 'normal' } ShareGlobals.data.biters_disabled = false @@ -152,7 +154,7 @@ return function(config) Task.set_timeout_in_ticks(1, do_waves, data) - game.print('Warning incomming biter attack! Number of waves: ' .. number_of_waves) + game.print('Warning incoming biter attack! Number of waves: ' .. number_of_waves) end local function rocket_launched(event) @@ -162,28 +164,16 @@ return function(config) return end - local pod = entity.cargo_pod - if not pod or not pod.valid then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite' }) then return end - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then - return - end - - local satellite_count = game.forces.player.get_item_launched('satellite') + local satellite_count = Rocket.get_item_launched({ + name = 'satellite', + force = 'player', + quality = win_data.extra_rockets_quality, + comparator = '>=' + }) if satellite_count == 0 then return end diff --git a/map_gen/maps/danger_ores/modules/rocket_launched_simple.lua b/map_gen/maps/danger_ores/modules/rocket_launched_simple.lua index 779012047..5c1a14390 100644 --- a/map_gen/maps/danger_ores/modules/rocket_launched_simple.lua +++ b/map_gen/maps/danger_ores/modules/rocket_launched_simple.lua @@ -1,4 +1,5 @@ local Event = require 'utils.event' +local Rocket = require 'utils.rocket' local RS = require 'map_gen.shared.redmew_surface' local Server = require 'features.server' local ShareGlobals = require 'map_gen.maps.danger_ores.modules.shared_globals' @@ -62,28 +63,16 @@ return function(config) return end - local pod = entity.cargo_pod - if not pod or not pod.valid then + if 0 == Rocket.count_rocket_contents(entity.cargo_pod, { name = 'satellite', quality = config.win_satellite_quality, comparator = '>=' }) then return end - local count = 0 - local qualities = prototypes.quality - for k = 1, pod.get_max_inventory_index() do - local inventory = pod.get_inventory(k) - if inventory then - local add = inventory.get_item_count - for tier, _ in pairs(qualities) do - count = count + add({ name = 'satellite', quality = tier }) - end - end - end - - if count == 0 then - return - end - - local satellite_count = game.forces.player.get_item_launched('satellite') + local satellite_count = Rocket.get_item_launched({ + name = 'satellite', + force = 'player', + quality = config.win_satellite_quality, + comparator = '>=', + }) if satellite_count == 0 then return end diff --git a/utils/inventory.lua b/utils/inventory.lua new file mode 100644 index 000000000..ca7a1a8b7 --- /dev/null +++ b/utils/inventory.lua @@ -0,0 +1,105 @@ +local Quality = require 'utils.quality' + +local Public = {} + +local function match_filter(stack, filter) + if filter == nil then + return true + end + + if type(filter) == 'string' then + filter = { name = filter } + end + + return (not filter.name or filter.name == stack.name) and + (not filter.quality or Quality.compare(stack.quality, filter.quality, filter.comparator or '=')) +end + +local function get_inventory_contents(inventory, filter) + local res = {} + if inventory and inventory.valid then + for _, stack in pairs(inventory.get_contents()) do + if match_filter(stack, filter) then + res[#res + 1] = stack + end + end + end + return res +end + +local function get_entity_contents(entity, filter) + local res = {} + local inv = entity.get_inventory + for k = 1, entity.get_max_inventory_index() do + for _, stack in pairs(get_inventory_contents(inv(k), filter)) do + res[#res + 1] = stack + end + end + return res +end + +-- Merge item with quality counts into the specified table +---@param tbl table +---@param stack ItemWithQualityCounts +Public.merge_item_with_quality_counts = function(tbl, stack) + if type(stack) == 'string' then + stack = { name = stack, count = 1 } + end + + local data = tbl[stack.name] or { count = 0 } + tbl[stack.name] = data + + data.count = data.count + stack.count + data[stack.quality] = (data[stack.quality] or 0) + stack.count +end + +-- Converts the specified contents into a dictionary based on the optional filter +---@param contents ItemWithQualityCounts[] +---@param filter? ItemFilter +---@return table +Public.to_dictionary = function(contents, filter) + local res = {} + for _, stack in pairs(contents) do + if match_filter(stack, filter) then + Public.merge_item_with_quality_counts(res, stack) + end + end + return res +end + +-- Retrieves contents from a LuaEntity or LuaInventory based on options +---@param LuaObject LuaEntity|LuaInventory +---@param filter? ItemFilter +---@return ItemWithQualityCounts[] +Public.get_contents = function(LuaObject, filter) + if LuaObject and LuaObject.valid then + if LuaObject.object_name == 'LuaEntity' then + return get_entity_contents(LuaObject, filter) + elseif LuaObject.object_name == 'LuaInventory' then + return get_inventory_contents(LuaObject, filter) + end + end + return {} +end + +-- Counts the total items based on a filter +---@param LuaObject LuaEntity|LuaInventory +---@param filter? ItemFilter +---@return number +Public.get_item_count = function(LuaObject, filter) + local res = 0 + for _, stack in pairs(Public.get_contents(LuaObject, filter)) do + res = res + stack.count + end + return res +end + +-- Returns a dictionary of item counts from the specified LuaObject +---@param LuaObject LuaEntity|LuaInventory +---@param filter? ItemFilter +---@return table +Public.get_contents_dictionary = function(LuaObject, filter) + return Public.to_dictionary(Public.get_contents(LuaObject, filter)) +end + +return Public diff --git a/utils/quality.lua b/utils/quality.lua new file mode 100644 index 000000000..bfdce3aa5 --- /dev/null +++ b/utils/quality.lua @@ -0,0 +1,60 @@ +local Public = {} + +local OPS = { + ['='] = function(v1, v2) return v1 == v2 end, + ['=='] = function(v1, v2) return v1 == v2 end, + ['>'] = function(v1, v2) return v1 > v2 end, + ['<'] = function(v1, v2) return v1 < v2 end, + ['≥'] = function(v1, v2) return v1 >= v2 end, + ['>='] = function(v1, v2) return v1 >= v2 end, + ['≤'] = function(v1, v2) return v1 <= v2 end, + ['<='] = function(v1, v2) return v1 <= v2 end, + ['≠'] = function(v1, v2) return v1 ~= v2 end, + ['!='] = function(v1, v2) return v1 ~= v2 end, + ['~='] = function(v1, v2) return v1 ~= v2 end, +} + +local level = function(quality) + if type(quality) == 'string' then + quality = prototypes.quality[quality] + end + return quality and quality.level or 0 +end + +-- Compare two quality identifiers based on the specified comparator +---@param q1 QualityID First quality ID +---@param q2 QualityID Second quality ID +---@param comparator string Comparison operator +---@return boolean Result of comparison +local compare = function(q1, q2, comparator) + if type(comparator) ~= 'string' then + error('Invalid comparator: expected string, got ' .. type(comparator)) + end + + local comparison_function = OPS[comparator] + if not comparison_function then + error('Invalid comparator: ' .. comparator) + end + + local l1 = level(q1) + local l2 = level(q2) + + return comparison_function(l1, l2) +end + +-- Create a wrapper function for the given comparator operation +local operation = function(comparator) + return function(q1, q2) + return compare(q1, q2, comparator) + end +end + +Public.compare = compare +Public.equal = operation('=') +Public.not_equal = operation('!=') +Public.greater_than = operation('>') +Public.less_than = operation('<') +Public.equal_or_greater_than = operation('>=') +Public.equal_or_less_than = operation('<=') + +return Public diff --git a/utils/rocket.lua b/utils/rocket.lua new file mode 100644 index 000000000..e35e60e3a --- /dev/null +++ b/utils/rocket.lua @@ -0,0 +1,115 @@ +local Event = require 'utils.event' +local Global = require 'utils.global' +local Inventory = require 'utils.inventory' +local Quality = require 'utils.quality' + +local Public = {} + +--[[ + Tracks all launched items per force in the global table. + The structure looks like this: + this = { + [force_index] = { + ['item_name'] = { normal = count1, uncommon = count2, count = total_count }, + }, + } +]] + +local this = {} +Global.register(this, function(tbl) this = tbl end) + +local function get_force_data(force_id) + if type(force_id) == 'userdata' then + force_id = force_id.index + end + this[force_id] = this[force_id] or {} + return this[force_id] +end + +-- Retrieve the contents of a rocket's cargo pod, optionally filtered +---@param rocket LuaEntity Represents the rocket entity +---@param filter? ItemFilter Optional filter for item selection +---@return ItemWithQualityCounts[] Array of items in the rocket +Public.get_rocket_contents = function(rocket, filter) + if not (rocket and rocket.valid) then + return {} + end + return Inventory.get_contents(rocket, filter) +end + +-- Count the total items in a rocket's cargo pod, optionally filtered +---@param rocket LuaEntity Represents the rocket entity +---@param filter? ItemFilter Optional filter for item selection +---@return number The total item count in the rocket +Public.count_rocket_contents = function(rocket, filter) + if not (rocket and rocket.valid) then + return 0 + end + return Inventory.get_item_count(rocket, filter) +end + +--- Get the count of a specific item launched from a given force +--- @param options table Contains parameters: {name = string, force? = ForceID, quality? = string, comparator? = string} +--- @return number The number of items launched +Public.get_item_launched = function(options) + local name = options.name + local force_id = options.force or 'player' -- Default to player if no force is specified + local quality = options.quality or 'count' -- Default to total count if no quality is specified + local comparator = options.comparator or '=' -- Default to equal to if no comparator is specified + + local force = game.forces[force_id] + if not (force and force.valid) then + return 0 + end + + local item_info = get_force_data(force.index)[name] + if item_info and quality ~= 'count' then + local sum = 0 + for tier, _ in pairs(prototypes.quality) do + if Quality.compare(tier, quality, comparator) then + sum = sum + (item_info[tier] or 0) + end + end + return sum + end + return (item_info and item_info[quality]) or 0 +end + +-- Event handler for when a rocket is launched; logs the items launched +-- Registered `on_rocket_launched` instead of `on_cargo_pod_finished_ascending` to allow other modules to rely on it +Event.add(defines.events.on_rocket_launched, function(event) + local force = event.rocket and event.rocket.force + if not force then + return + end + + local logs = get_force_data(force) + for _, stack in pairs(Inventory.get_contents(event.rocket.cargo_pod)) do + Inventory.merge_item_with_quality_counts(logs, stack) + if _DEBUG then + game.print(string.format('Launched: name = %s | quality = %s | count = %d', stack.name, stack.quality, stack.count)) + end + end +end) + +-- Event handler for resetting a force's logs +Event.add(defines.events.on_force_reset, function(event) + this[event.force.index] = {} +end) + +-- Event handler for merging logs from one force to another during force merging +Event.add(defines.events.on_forces_merging, function(event) + local src = get_force_data(event.source) + local dst = get_force_data(event.destination) + + for name, info in pairs(src) do + dst[name] = dst[name] or {} + for k, v in pairs(info) do + dst[name][k] = (dst[name][k] or 0) + v + end + end + + this[event.source.index] = nil +end) + +return Public