Добавлены все обновления от сообщества, вплоть до #148

This commit is contained in:
2024-09-12 14:28:43 +03:00
parent 98159766c4
commit 487a0e6e16
8841 changed files with 23077 additions and 20175 deletions

View File

@@ -0,0 +1,645 @@
---Gets or makes playerdata table.
---@param player_index uint LuaPlayer index
---@return Playerdata playerdata
function get_make_playerdata(player_index)
local playerdata = global.playerdata[player_index]
if not playerdata then
playerdata = {
luaplayer=game.players[player_index],
index=player_index,
is_active=false,
job={},
logistic_requests={},
gui={},
options={}
}
global.playerdata[player_index] = playerdata
end
return playerdata
end
---Returns an empty request table for the given item.
---@param name string Item name
---@return Request request
function make_empty_request(name)
return {name=name, count=0, inventory=0, logistic_request={}}
end
---Sorts a table of `Request` objects by count, in descending order.
---@param requests table<string, Request> Table of requests to be sorted
---@return Request[] requests_sorted
function sort_requests(requests)
local requests_sorted = {}
for _, request in pairs(requests) do table.insert(requests_sorted, request) end
table.sort(requests_sorted, function(a, b)
if a.count > b.count then
return true
elseif a.count < b.count then
return false
elseif a.name < b.name then
return true
else
return false
end
end)
return requests_sorted
end
---Iterates over passed entities and counts items needed to build all ghost entities and tiles.
---@param entities LuaEntity[] table of entities
---@param ignore_tiles boolean Determines whether ghost tiles are counted
---@return table<uint, LuaEntity> ghosts table of actual ghost entities/tiles
---@return table requests table of requests, indexed by request name
function get_selection_counts(entities, ignore_tiles)
local ghosts, requests = {}, {}
local cache = {}
-- Iterate over entities and filter out anything that's not a ghost
local insert = table.insert
for _, entity in pairs(entities) do
local entity_type = entity.type
if entity_type == "entity-ghost" or (entity_type == "tile-ghost" and not ignore_tiles) then
local ghost_name = entity.ghost_name
local unit_number = entity.unit_number --[[@as uint]]
-- Get item to place entity, from prototype if necessary
if not cache[ghost_name] then
local prototype = entity_type == "entity-ghost" and
game.entity_prototypes[ghost_name] or
game.tile_prototypes[ghost_name]
cache[ghost_name] = {
item=prototype.items_to_place_this and prototype.items_to_place_this[1] or nil
}
end
ghosts[unit_number] = {}
-- If entity is associated with item, increment request for that item by `item.count`
local item = cache[ghost_name].item
if item then
requests[item.name] = requests[item.name] or make_empty_request(item.name)
requests[item.name].count = requests[item.name].count + item.count
insert(ghosts[unit_number], item)
end
-- If entity has module requests, increment request for each module type
local item_requests = entity_type == "entity-ghost" and entity.item_requests or nil
if item_requests and table_size(item_requests) > 0 then
for name, val in pairs(item_requests) do
requests[name] = requests[name] or make_empty_request(name)
requests[name].count = requests[name].count + val
insert(ghosts[unit_number], {name=name, count=val})
end
end
script.register_on_entity_destroyed(entity)
elseif entity_type == "item-request-proxy" then
local unit_number = entity.unit_number --[[@as uint]]
ghosts[unit_number] = {}
for name, val in pairs(entity.item_requests) do
requests[name] = requests[name] or make_empty_request(name)
requests[name].count = requests[name].count + val
insert(ghosts[unit_number], {name=name, count=val})
end
script.register_on_entity_destroyed(entity)
elseif entity.to_be_upgraded() then
local unit_number = entity.unit_number --[[@as uint]]
local prototype = entity.get_upgrade_target() --[[@as LuaEntityPrototype]]
local ghost_name = prototype.name
-- Get item to place entity, from prototype if necessary
if not cache[ghost_name] then
cache[ghost_name] = {
item=prototype.items_to_place_this and prototype.items_to_place_this[1] or nil
}
end
ghosts[unit_number] = {}
-- If entity is associated with item, increment request for that item by `item.count`
local item = cache[ghost_name].item
if item then
requests[item.name] = requests[item.name] or make_empty_request(item.name)
requests[item.name].count = requests[item.name].count + item.count
insert(ghosts[unit_number], item)
end
script.register_on_entity_destroyed(entity)
end
end
return ghosts, requests
end
---Returns the blueprint tiles contained within a given item stack.
---@param item_stack LuaItemStack Must be a blueprint or a blueprint-book
---@return Tile[] tiles
function get_blueprint_tiles(item_stack)
if item_stack.is_blueprint_book then
local inventory = item_stack.get_inventory(defines.inventory.item_main) --[[@as LuaInventory]]
return get_blueprint_tiles(inventory[item_stack.active_index])
else
return (item_stack.get_blueprint_tiles() or {})
end
end
---Processes blueprint entities and tiles to generate item request counts.
---@param entities table array of blueprint entities
---@param tiles table array of blueprint tiles
---@return table requests
function get_blueprint_counts(entities, tiles)
local requests = {}
local cache = {}
-- Iterate over blueprint entities
for _, entity in pairs(entities) do
if not cache[entity.name] then
local prototype = game.entity_prototypes[entity.name]
cache[entity.name] = {
item=prototype.items_to_place_this and prototype.items_to_place_this[1] or nil
}
end
-- If entity is associated with item, increment request for that item by `item.count`
local item = cache[entity.name].item
if item then
requests[item.name] = requests[item.name] or make_empty_request(item.name)
requests[item.name].count = requests[item.name].count + item.count
end
-- If entity has module requests, increment request for each module type
local item_requests = entity.items
if item_requests and table_size(item_requests) > 0 then
for name, val in pairs(item_requests) do
requests[name] = requests[name] or make_empty_request(name)
requests[name].count = requests[name].count + val
end
end
end
-- Iterate over blueprint tiles
for _, tile in pairs(tiles) do
if not cache[tile.name] then
local prototype = game.tile_prototypes[tile.name]
cache[tile.name] = {
item=prototype.items_to_place_this and prototype.items_to_place_this[1] or nil
}
end
-- If tile is associated with item, increment request for that item by `item.count`
local item = cache[tile.name].item
if item then
requests[item.name] = requests[item.name] or make_empty_request(item.name)
requests[item.name].count = requests[item.name].count + item.count
end
end
return requests
end
---Converts a given player's `Request` table to signals out of a series of constant combinators.
---@param player_index uint Player index
function make_combinators_blueprint(player_index)
local playerdata = get_make_playerdata(player_index)
-- Make sure constant combinator prototype exists
local prototype = game.entity_prototypes["constant-combinator"]
if not prototype then
playerdata.luaplayer.print({"ghost-counter-message.missing-constant-combinator-prototype"})
return
end
local n_slots = prototype.item_slot_count
local requests = playerdata.job.requests_sorted
local request_index = 1
local combinators = {}
-- Iterate over the number of constant combinators we will need
for i = 1, math.ceil(#requests / n_slots) do
combinators[i] = {
entity_number=i,
name="constant-combinator",
position={i - 0.5, 0},
direction=4,
control_behavior={filters={}},
connections={{}}
}
local filters = combinators[i].control_behavior.filters
-- Set the combinator slots to the ghost request counts
for j = 1, n_slots do
local request = requests[request_index]
filters[j] = {signal={type="item", name=request.name}, count=request.count, index=j}
-- Increment request index; break if no more requests are left
request_index = request_index + 1
if request_index > #requests then break end
end
end
-- Wire up the combinators to one another
if #combinators > 1 then
for i = 1, (#combinators - 1) do
local connections = combinators[i].connections[1]
connections["green"] = {{entity_id=i + 1}}
connections["red"] = {{entity_id=i + 1}}
end
end
-- Try to clear the cursor
local is_successful = playerdata.luaplayer.clear_cursor()
if is_successful then
playerdata.luaplayer.cursor_stack.set_stack("blueprint")
playerdata.luaplayer.cursor_stack.set_blueprint_entities(combinators)
else
playerdata.luaplayer.print({"ghost-counter-message.failed-to-clear-cursor"})
end
end
---Deletes requests with zero ghosts from the `job.requests` table.
---@param player_index uint Player index
function remove_empty_requests(player_index)
local playerdata = get_make_playerdata(player_index)
for name, request in pairs(playerdata.job.requests) do
if request.count <= 0 then playerdata.job.requests[name] = nil end
end
end
---Updates table of `Request`s with inventory and cursor stack contents.
---@param player_index uint Player index
function update_inventory_info(player_index)
local playerdata = get_make_playerdata(player_index)
local cursor_stack = playerdata.luaplayer.cursor_stack
local inventory = playerdata.luaplayer.get_main_inventory()
local contents = inventory and inventory.get_contents() or {}
local requests = playerdata.job.requests
-- Iterate over each request and get the count in inventory
for name, request in pairs(requests) do request.inventory = contents[name] or 0 end
-- Add cursor contents to request count
if cursor_stack and cursor_stack.valid_for_read and requests[cursor_stack.name] then
local request = requests[cursor_stack.name]
request.inventory = request.inventory + cursor_stack.count
end
end
---Updates table of `Request`s with the player's current logistic requests.
---@param player_index uint Player index
function update_logistics_info(player_index)
local playerdata = get_make_playerdata(player_index)
local requests = playerdata.job.requests
-- Get player character
local character = playerdata.luaplayer.character
if not character then return end
-- Iterate over each logistic slot and update request table with logistic request details
local logistic_requests = {}
for i = 1, character.request_slot_count do
local slot = playerdata.luaplayer.get_personal_logistic_slot(i --[[@as uint]])
if requests[slot.name] then
requests[slot.name].logistic_request = {slot_index=i, min=slot.min, max=slot.max}
logistic_requests[slot.name] = true
end
end
-- Clear the `logistic_request` table of the request if one was not found
for _, request in pairs(playerdata.job.requests) do
if not logistic_requests[request.name] then request.logistic_request = {} end
end
end
---Iterates over one-time requests table and restores old requests if they have been fulfilled.
---@param player_index uint Player index
function update_one_time_logistic_requests(player_index)
local playerdata = get_make_playerdata(player_index)
if not playerdata.luaplayer.character then return end
local inventory = playerdata.luaplayer.get_main_inventory() --[[@as LuaInventory]]
-- Iterate over one-time requests table and restore old requests if they have been fulfilled
for name, logi_req in pairs(playerdata.logistic_requests) do
local request = playerdata.job.requests[name]
local slot = playerdata.luaplayer.get_personal_logistic_slot(logi_req.slot_index)
if request then
-- Update logistic request to reflect new ghost count
if slot.min ~= request.count then
local new_slot = {name=name, min=request.count}
logi_req.new_min = request.count
logi_req.is_new = true
playerdata.luaplayer.set_personal_logistic_slot(logi_req.slot_index, new_slot)
end
-- Restore prior request (if any) if one-time request has been fulfilled
if (inventory.get_item_count(name) >= logi_req.new_min) or
(logi_req.new_min <= (logi_req.old_min or 0)) then
restore_prior_logistic_request(player_index, name)
end
end
end
end
---Iterates over player's logistic slots and returns the first empty slot. Player _must_ have a
---character entity.
---@param player_index uint Player index
---@return uint? slot_index First empty slot
function get_first_empty_slot(player_index)
local playerdata = get_make_playerdata(player_index)
local character = playerdata.luaplayer.character --[[@as LuaEntity]]
for slot_index = 1, character.request_slot_count + 1 do
---@cast slot_index uint
local slot = playerdata.luaplayer.get_personal_logistic_slot(slot_index)
if slot.name == nil then return slot_index end
end
end
---Gets a table with details of any existing logistic request for a given item.
---@param player_index uint Player index
---@param name string Item name
---@return table|nil logistic_request
function get_existing_logistic_request(player_index, name)
local playerdata = get_make_playerdata(player_index)
local character = playerdata.luaplayer.character
if not character then return nil end
for i = 1, character.request_slot_count do
---@cast i uint
local slot = playerdata.luaplayer.get_personal_logistic_slot(i)
if slot and slot.name == name then
return {slot_index=i, name=slot.name, min=slot.min, max=slot.max}
end
end
end
---Generates a logistic request or modifies an existing request to satisfy need. Registers the
---change in a `playerdata.logistic_requests` table so that it can be reverted later on.
---@param player_index uint Player index
---@param name string `request` name
function make_one_time_logistic_request(player_index, name)
-- Abort if no player character
local playerdata = get_make_playerdata(player_index)
if not playerdata.luaplayer.character then return end
-- Abort if player already has more of item in inventory than needed
local request = playerdata.job.requests[name]
if not request or request.inventory >= request.count then return end
-- Get any existing request and abort if it would already meet need
local existing_request = get_existing_logistic_request(player_index, request.name) or {}
if (existing_request.min or 0) >= request.count then return end
-- Prepare new logistic slot and get existing or first empty `slot_index`
local new_slot = {name=request.name, min=request.count}
local slot_index = existing_request.slot_index or get_first_empty_slot(player_index)
if not slot_index then return end
-- Save details of change in playerdata so that it can be reverted later
-- This is set here in order for the event handler to be able to identify this change
-- as originating from the mod and to ignore it.
playerdata.logistic_requests[request.name] = {
slot_index=slot_index,
old_min=existing_request.min,
old_max=existing_request.max,
new_min=request.count,
is_new=true
}
-- Actually modify personal logistic slot
local is_successful = playerdata.luaplayer.set_personal_logistic_slot(slot_index, new_slot)
if is_successful then
-- Update request's `logistic_request` table
request.logistic_request.slot_index = slot_index
request.logistic_request.min = request.count
request.logistic_request.max = nil
playerdata.has_updates = true
register_update(player_index, game.tick)
else
-- Delete record of temporary request as it didn't go through
playerdata.logistic_requests[request.name] = nil
end
end
---Restores the prior logistic request (if any) that was in place before the one-time request was
---made.
---@param player_index uint Player index
---@param name string Item name
function restore_prior_logistic_request(player_index, name)
local playerdata = get_make_playerdata(player_index)
if not playerdata.luaplayer.character then return end
local request = playerdata.logistic_requests[name]
local slot
-- Either clear or reset slot using old request values
if request.old_min or request.old_max then
slot = {name=name, min=request.old_min, max=request.old_max}
playerdata.luaplayer.set_personal_logistic_slot(request.slot_index, slot)
else
playerdata.luaplayer.clear_personal_logistic_slot(request.slot_index)
end
if playerdata.job.requests[name] then
if slot then
playerdata.job.requests[name].logistic_request = {
slot_index=request.slot_index,
min=slot.min,
max=slot.max
}
else
playerdata.job.requests[name].logistic_request = {}
end
end
end
---Iterates over `playerdata.logistic_requests` to get rid of them.
---@param player_index uint Player index
function cancel_all_one_time_requests(player_index)
local playerdata = get_make_playerdata(player_index)
for name, _ in pairs(playerdata.logistic_requests) do
restore_prior_logistic_request(player_index, name)
end
end
---Returns the yield of a given item from a single craft of a given recipe.
---@param item_name string Item name
---@param recipe LuaRecipePrototype Recipe prototype
---@return number
function get_yield_per_craft(item_name, recipe)
local yield = 0
for _, product in pairs(recipe.products) do
if product.name == item_name then
local probability = product.probability or 1
yield = (product.amount) and (product.amount * probability) or
((product.amount_min + product.amount_max) * 0.5 * probability)
break
end
end
return yield
end
---Returns the number of times an item is set to be produced by a given character, taking into
---account their crafting queue contents.
---@param character LuaEntity Character entity
---@param item_name string Name of item to craft
---@return uint item_count
function get_item_count_from_character_crafting_queue(character, item_name)
if character.crafting_queue_size == 0 then return 0 end
local relevant_recipes = game.get_filtered_recipe_prototypes{
{filter="has-product-item", elem_filters={{filter="name", name=item_name}}},
{filter="hidden-from-player-crafting", invert=true, mode="and"}
}
local unique_recipes = {} --[[@as table<string, uint>]]
local item_count = 0
-- Create a list of unique and relevant recipes in the crafting queue
for _, queue_item in pairs(character.crafting_queue) do
local recipe_name = queue_item.recipe
if not queue_item.prerequisite and (unique_recipes[recipe_name] or relevant_recipes[recipe_name]) then
unique_recipes[recipe_name] = (unique_recipes[recipe_name] or 0) + queue_item.count
end
end
-- Count number of `name` items that will ultimately be produced by recipes in crafting queue
for recipe_name, n_crafts in pairs(unique_recipes) do
local yield_per_craft = get_yield_per_craft(item_name, relevant_recipes[recipe_name])
item_count = item_count + math.floor(yield_per_craft * n_crafts)
end
return item_count --[[@as uint]]
end
---Crafts a given item; amount to craft based on the corresponding request for that item.
---@param player_index uint Player index
---@param request Request Request data
---@return "no-character"|"no-crafts-needed"|"attempted" result
---@return uint? items_crafted Number of items crafted
function craft_request(player_index, request)
-- Abort if no player character
local playerdata = get_make_playerdata(player_index)
local character = playerdata.luaplayer.character
if not character then return "no-character" end
-- Abort if player already has more of item in inventory than needed
if request.inventory >= request.count then return "no-crafts-needed" end
-- Calculate item need; abort if 0 (or less)
local crafting_yield = get_item_count_from_character_crafting_queue(character, request.name)
local item_need = request.count - request.inventory - crafting_yield
local original_need = item_need
if item_need <= 0 then return "no-crafts-needed" end
local crafting_recipes = game.get_filtered_recipe_prototypes{
{filter="has-product-item", elem_filters={{filter="name", name=request.name}}},
{filter="hidden-from-player-crafting", invert=true, mode="and"}
}
for recipe_name, recipe in pairs(crafting_recipes) do
local yield_per_craft = get_yield_per_craft(request.name, recipe)
local needed_crafts = math.ceil(item_need / yield_per_craft) --[[@as uint]]
local actual_crafts = character.begin_crafting{recipe=recipe_name, count=needed_crafts, silent=true}
item_need = item_need - math.floor(actual_crafts * yield_per_craft)
if item_need <= 0 then break end
end
return "attempted", original_need - item_need
end
---Registers that a change in data tables has occured and marks the responsible player as having
---data updates to process.
---@param player_index uint Player index
---@param tick number Tick during which the data update occurred
function register_update(player_index, tick)
local playerdata = get_make_playerdata(player_index)
-- Mark player as having a data update, in order for it to get reprocessed
playerdata.has_updates = true
-- Record the tick in which the update was registered
global.last_event = tick
-- Register nth_tick handler if needed
register_nth_tick_handler(true)
end
---Registers/unregisters on_nth_tick event handler.
---@param state any
function register_nth_tick_handler(state)
if state and not global.events.nth_tick then
global.events.nth_tick = true
script.on_nth_tick(global.settings.min_update_interval, on_nth_tick)
elseif state == false and global.events.nth_tick then
global.events.nth_tick = false
---@diagnostic disable-next-line
script.on_nth_tick(nil)
end
end
---Registers/unregisters event handlers for inventory or player cursor stack changes.
---@param state boolean Determines whether to register or unregister event handlers
function register_inventory_monitoring(state)
if state and not global.events.inventory then
global.events.inventory = true
script.on_event(defines.events.on_player_main_inventory_changed,
on_player_main_inventory_changed)
script.on_event(defines.events.on_player_cursor_stack_changed,
on_player_main_inventory_changed)
script.on_event(defines.events.on_entity_destroyed, on_ghost_destroyed)
elseif state == false and global.events.inventory then
global.events.inventory = false
script.on_event(defines.events.on_player_main_inventory_changed, nil)
script.on_event(defines.events.on_player_cursor_stack_changed, nil)
script.on_event(defines.events.on_entity_destroyed, nil)
end
end
---Registers/unregisters event handlers for player logistic slot changes.
---@param state boolean Determines whether to register or unregister event handlers
function register_logistics_monitoring(state)
if state and not global.events.logistics then
global.events.logistics = true
script.on_event(defines.events.on_entity_logistic_slot_changed,
on_entity_logistic_slot_changed)
elseif state == false and global.events.logistics then
global.events.logistics = false
script.on_event(defines.events.on_entity_logistic_slot_changed, nil)
end
end
---Iterates over global playerdata table and determines whether any connected players have their
---mod GUI open.
---@return boolean
function is_inventory_monitoring_needed()
for _, playerdata in pairs(global.playerdata) do
if playerdata.is_active and playerdata.luaplayer.connected then return true end
end
return false
end
---Iterates over the global playerdata table and checks to see if any one-time logistic requests
---are still unfulfilled.
---@return boolean
function is_logistics_monitoring_needed()
for _, playerdata in pairs(global.playerdata) do
if (playerdata.is_active or table_size(playerdata.logistic_requests) > 0) and
playerdata.luaplayer.connected then return true end
end
return false
end

View File

@@ -0,0 +1,201 @@
---Handles the lua shortcut button or hotkey being triggered.
---@param event EventData.on_lua_shortcut|EventData.CustomInputEvent Event table
function on_lua_shortcut(event)
-- Exclude irrelevant lua shortcuts
if event.prototype_name and event.prototype_name ~= NAME.shortcut.button then return end
local player = game.get_player(event.player_index) --[[@as LuaPlayer]]
local cursor_stack = player.cursor_stack
-- Abort if player has no cursor stack as they are presumably a spectator
if not cursor_stack then return end
if player.is_cursor_blueprint() then
on_player_selected_blueprint(event)
else
local clear_cursor = player.clear_cursor()
if clear_cursor then
cursor_stack.set_stack({name=NAME.tool.ghost_counter})
end
end
end
script.on_event(defines.events.on_lua_shortcut, on_lua_shortcut)
script.on_event(NAME.input.ghost_counter_selection, on_lua_shortcut)
---Event handler for selection using GC tool
---@param event EventData.on_player_selected_area Event table
---@param ignore_tiles boolean Determines whether tiles are included in count
function on_player_selected_area(event, ignore_tiles)
if event.item ~= NAME.tool.ghost_counter then return end
local ghosts, requests = get_selection_counts(event.entities, ignore_tiles)
-- Open window only if there are non-zero ghost entities
if table_size(requests) > 0 then
local player_index = event.player_index
local playerdata = get_make_playerdata(player_index)
playerdata.job = {
area=event.area,
ghosts=ghosts,
requests=requests,
requests_sorted=sort_requests(requests)
}
update_one_time_logistic_requests(player_index)
update_inventory_info(player_index)
update_logistics_info(player_index)
Gui.toggle(player_index, true)
playerdata.luaplayer.clear_cursor()
end
end
script.on_event(defines.events.on_player_selected_area,
---@param event EventData.on_player_selected_area
function(event) on_player_selected_area(event, true) end)
script.on_event(defines.events.on_player_alt_selected_area,
---@param event EventData.on_player_selected_area
function(event) on_player_selected_area(event, false) end)
---Handles Ghost counter being activated with a blueprint in cursor.
---@param event EventData.on_lua_shortcut|EventData.CustomInputEvent Event table
function on_player_selected_blueprint(event)
local player_index = event.player_index
local playerdata = get_make_playerdata(player_index)
local entities = playerdata.luaplayer.get_blueprint_entities() or {}
local tiles = {}
if (playerdata.luaplayer.is_cursor_blueprint() and
playerdata.luaplayer.cursor_stack.valid_for_read) then
tiles = get_blueprint_tiles(playerdata.luaplayer.cursor_stack)
end
-- Abort if player not holding blueprint or empty blueprint
if not (entities and #entities > 0) and not (tiles and #tiles > 0) then return end
local requests = get_blueprint_counts(entities, tiles)
playerdata.job = {
area={},
ghosts={},
requests=requests,
requests_sorted=sort_requests(requests)
}
update_one_time_logistic_requests(player_index)
update_inventory_info(player_index)
update_logistics_info(player_index)
Gui.toggle(player_index, true)
end
---Updates playerdata.job.requests table as well as one-time requests to see if any can be
---considered fulfilled
---@param event EventData.on_player_main_inventory_changed Event table
function on_player_main_inventory_changed(event)
local playerdata = get_make_playerdata(event.player_index)
if not playerdata.is_active then return end
if playerdata.luaplayer.controller_type ~= defines.controllers.character then return end
register_update(playerdata.index, event.tick)
end
---Updates one-time logistic requests table as well as job.requests
---@param event EventData.on_entity_logistic_slot_changed Event table
function on_entity_logistic_slot_changed(event)
-- Exit if event does not involve a player character
if event.entity.type ~= "character" then return end
local player = event.entity.player or event.entity.associated_player
if not player then return end
local player_index = player.index
local playerdata = get_make_playerdata(player_index)
-- Iterate over known one-time logistic requests to see if the event concerns any of them
for name, request in pairs(playerdata.logistic_requests) do
if request.slot_index == event.slot_index then
if request.is_new then
request.is_new = false
return
else
playerdata.logistic_requests[name] = nil
end
break
end
end
register_update(player_index, event.tick)
end
---Triggers an update if the player respawns.
---@param event EventData.on_player_respawned Event data
function on_player_respawned(event)
local playerdata = get_make_playerdata(event.player_index)
if not playerdata.is_active then return end
register_update(playerdata.index, event.tick)
end
script.on_event(defines.events.on_player_respawned, on_player_respawned)
---Triggers an update if the player dies.
---@param event EventData.on_player_died Event data
function on_player_died(event)
local playerdata = get_make_playerdata(event.player_index)
if not playerdata.is_active then return end
register_update(playerdata.index, event.tick)
end
script.on_event(defines.events.on_player_died, on_player_died)
---Handles `on_entity_destroyed` by looking up `event.unit_number` in ghost tables and updates
---requests tables where appropriate
---@param event EventData.on_entity_destroyed Event table
function on_ghost_destroyed(event)
-- Since even ghost tiles have `unit_number`, exit if none is provided
if not event.unit_number then return end
-- Iterate over each player, and update their requests if they were tracking the entity
for player_index, playerdata in pairs(global.playerdata) do
if playerdata.is_active and playerdata.job.ghosts[event.unit_number] then
local items = playerdata.job.ghosts[event.unit_number]
for _, item in pairs(items) do
local request = playerdata.job.requests[item.name]
request.count = request.count - item.count
end
register_update(player_index, event.tick)
end
end
end
---Handles `on_nth_tick`—processes data for players who have had data updates. Aborts and
---unregisters itself if there were no data updates for any player since the previous function call
---@param event table Event table
function on_nth_tick(event)
-- If no data updates happened over the last 5 ticks, unregister nth_tick handler and exit
if event.tick - global.last_event > global.settings.min_update_interval then
register_nth_tick_handler(false)
return
end
-- Iterate over each player and process their data if they had updates
for player_index, playerdata in pairs(global.playerdata) do
-- If a player had registered data updates, reprocess their data
if playerdata.has_updates then
update_one_time_logistic_requests(player_index)
if playerdata.is_active then
update_inventory_info(player_index)
update_logistics_info(player_index)
Gui.update_list(player_index)
end
-- Reset `has_updates` boolean for that player
playerdata.has_updates = false
end
end
-- Check if event handlers can be unbound
if not is_inventory_monitoring_needed() then register_inventory_monitoring(false) end
if not is_logistics_monitoring_needed() then register_logistics_monitoring(false) end
end

View File

@@ -0,0 +1,420 @@
Gui = {}
---Toggles mod GUI on or off
---@param player_index uint Player index
---@param state boolean true -> on, false -> off
function Gui.toggle(player_index, state)
local playerdata = get_make_playerdata(player_index)
state = state or not playerdata.is_active
if state then
playerdata.is_active = true
-- Create mod gui and register event handler
Gui.make_gui(player_index)
register_inventory_monitoring(true)
register_logistics_monitoring(true)
else
playerdata.is_active = false
playerdata.job = {
area={},
ghosts={},
requests={},
requests_sorted={}
}
-- Destroy mod GUI and remove references to it
if playerdata.gui.root and playerdata.gui.root.valid then
local last_location = playerdata.gui.root.location --[[@as GuiLocation.0]]
playerdata.gui.root.destroy()
playerdata.gui = {last_location=last_location}
end
-- Unbind event hooks if no no longer needed
if not is_inventory_monitoring_needed() then register_inventory_monitoring(false) end
if not is_logistics_monitoring_needed() then register_logistics_monitoring(false) end
end
end
---Make mod GUI
---@param player_index uint Player index
function Gui.make_gui(player_index)
local playerdata = get_make_playerdata(player_index)
local screen = playerdata.luaplayer.gui.screen
local window_loc_x, window_loc_y
-- Restore previous saved location, if any
if playerdata.gui.last_location then
local location = playerdata.gui.last_location
local resolution = playerdata.luaplayer.display_resolution
window_loc_x = location.x < resolution.width and location.x or nil
window_loc_y = location.y < resolution.height and location.y or nil
end
-- Destory existing mod GUI if one exists
if screen[NAME.gui.root_frame] then
local location = screen[NAME.gui.root_frame].location
window_loc_x, window_loc_y = location.x, location.y
screen[NAME.gui.root_frame].destroy()
end
playerdata.gui.root = screen.add{
type="frame",
name=NAME.gui.root_frame,
direction="vertical",
style=NAME.style.root_frame
}
do
local resolution = playerdata.luaplayer.display_resolution
local x = window_loc_x or 50
local y = window_loc_y or (resolution.height / 2) - 300
playerdata.gui.root.location = {x, y}
end
-- Create title bar
local titlebar_flow = playerdata.gui.root.add{
type="flow",
direction="horizontal",
style=NAME.style.titlebar_flow
}
titlebar_flow.drag_target = playerdata.gui.root
titlebar_flow.add{
type="label",
caption="Ghost Counter",
ignored_by_interaction=true,
style="frame_title"
}
titlebar_flow.add{
type="empty-widget",
ignored_by_interaction=true,
style=NAME.style.titlebar_space_header
}
local hide_empty = playerdata.options.hide_empty_requests
titlebar_flow.add{
type="sprite-button",
name=NAME.gui.hide_empty_button,
tooltip={"ghost-counter-gui.hide-empty-requests-tooltip"},
sprite=hide_empty and NAME.sprite.hide_empty_black or NAME.sprite.hide_empty_white,
hovered_sprite=NAME.sprite.hide_empty_black,
clicked_sprite=hide_empty and NAME.sprite.hide_empty_white or NAME.sprite.hide_empty_black,
style=hide_empty and NAME.style.titlebar_button_active or NAME.style.titlebar_button
}
titlebar_flow.add{
type="sprite-button",
name=NAME.gui.close_button,
sprite="utility/close_white",
hovered_sprite="utility/close_black",
clicked_sprite="utility/close_black",
tooltip={"ghost-counter-gui.close-button-tooltip"},
style="close_button"
}
local deep_frame = playerdata.gui.root.add{
type="frame",
direction="vertical",
style=NAME.style.inside_deep_frame
}
local toolbar = deep_frame.add{
type="frame",
direction="horizontal",
style=NAME.style.topbar_frame
}
toolbar.add{
type="sprite-button",
name=NAME.gui.get_signals_button,
sprite=NAME.sprite.get_signals_white,
hovered_sprite=NAME.sprite.get_signals_black,
clicked_sprite=NAME.sprite.get_signals_black,
tooltip={"ghost-counter-gui.get-signals-tooltip"},
style=NAME.style.get_signals_button
}
toolbar.add{
type="empty-widget",
style=NAME.style.topbar_space
}
toolbar.add{
type="sprite-button",
name=NAME.gui.craft_all_button,
sprite=NAME.sprite.craft_all_white,
hovered_sprite=NAME.sprite.craft_all_black,
clicked_sprite=NAME.sprite.craft_all_black,
tooltip={"ghost-counter-gui.craft-all-tooltip"},
style=NAME.style.get_signals_button
}
toolbar.add{
type="button",
name=NAME.gui.request_all_button,
caption={"ghost-counter-gui.request-all-caption"},
tooltip={"ghost-counter-gui.request-all-tooltip"},
style=NAME.style.ghost_request_all_button
}
toolbar.add{
type="sprite-button",
name=NAME.gui.cancel_all_button,
sprite=NAME.sprite.cancel_white,
hovered_sprite=NAME.sprite.cancel_black,
clicked_sprite=NAME.sprite.cancel_black,
tooltip={"ghost-counter-gui.cancel-all-tooltip"},
style=NAME.style.ghost_cancel_all_button
}
playerdata.gui.requests_container = deep_frame.add{
type="scroll-pane",
name=NAME.gui.scroll_pane,
style=NAME.style.scroll_pane
}
Gui.make_list(player_index)
end
---Creates the list of request frames in the GUI
---@param player_index uint Player index
function Gui.make_list(player_index)
local playerdata = get_make_playerdata(player_index)
-- Create a new row frame for each request
playerdata.gui.requests = {}
for _, request in pairs(playerdata.job.requests_sorted) do
Gui.make_row(player_index, request)
end
end
---Returns request button properties based on request fulfillment and other criteria
---@param request table `request` table
---@param one_time_request table `playerdata.logistic_requests[request.name]`
---@return boolean enabled Whehter button should be enabled
---@return string style Style that should be applied to the button
---@return LocalisedString tooltip Tooltip shown for button
function make_request_button_properties(request, one_time_request)
local logistic_request = request.logistic_request or {}
local enabled = ((logistic_request.min or 0) < request.count) or one_time_request and true or
false
local style =
((logistic_request.min or 0) < request.count) and NAME.style.ghost_request_button or
NAME.style.ghost_request_active_button
local str = "[item=" .. request.name .. "] "
local tooltip
if enabled then
tooltip = ((logistic_request.min or 0) < request.count) and
{"ghost-counter-gui.set-temporary-request-tooltip", request.count, str} or
{"ghost-counter-gui.unset-temporary-request-tooltip"}
else
tooltip = {"ghost-counter-gui.existing-logistic-request-tooltip"}
end
return enabled, style, tooltip
end
---Updates the list of request frames in the GUI
---@param player_index uint Player index
function Gui.update_list(player_index)
local playerdata = get_make_playerdata(player_index)
if not playerdata.is_active or not playerdata.gui.requests then return end
local indices = {count=1, sprite=2, label=3, inventory=4, request=5}
-- Update gui elements with new values
for name, frame in pairs(playerdata.gui.requests) do
local request = playerdata.job.requests[name]
if request.count > 0 or not playerdata.options.hide_empty_requests then
frame.visible = true
-- Update ghost count
frame.children[indices.count].caption = request.count
-- Update amont in inventory
frame.children[indices.inventory].caption = request.inventory
-- Calculate amount missing
local diff = request.count - request.inventory
-- If amount needed exceeds amount in inventory, show request button
local request_element = frame.children[indices.request]
if diff > 0 then
local enabled, style, tooltip = make_request_button_properties(request,
playerdata.logistic_requests[request.name])
if request_element.type == "button" then
request_element.enabled = enabled
request_element.style = style
request_element.caption = diff
request_element.tooltip = tooltip
else
frame.children[indices.request].destroy()
frame.add{
type="button",
caption=diff,
enabled=enabled,
style=style,
tooltip=tooltip,
tags={ghost_counter_request=request.name}
}
end
-- Otherwise create request-fulfilled checkmark previous element was a request button
elseif request_element.type == "button" then
request_element.destroy()
local sprite_container = frame.add{
type="flow",
direction="horizontal",
style=NAME.style.ghost_request_fulfilled_flow
}
sprite_container.add{
type="sprite",
sprite="utility/check_mark_white",
resize_to_sprite=false,
style=NAME.style.ghost_request_fulfilled_sprite
}
end
else
frame.visible = false
end
end
end
---Generates the row frame for a given request table
---@param player_index uint Player index
---@param request table `request` table, containing name, count, inventory, etc.
function Gui.make_row(player_index, request)
local playerdata = get_make_playerdata(player_index)
local parent = playerdata.gui.requests_container
local localized_name = game.item_prototypes[request.name].localised_name
-- Row frame
local frame = parent.add{type="frame", direction="horizontal", style=NAME.style.row_frame}
playerdata.gui.requests[request.name] = frame
-- Ghost (item) count
frame.add{type="label", caption=request.count, style=NAME.style.ghost_number_label}
-- Item sprite
frame.add{
type="sprite",
sprite="item/" .. request.name,
resize_to_sprite=false,
style=NAME.style.ghost_sprite
}
-- Item or tile localized name
frame.add{type="label", caption=localized_name, style=NAME.style.ghost_name_label}
-- Amount in inventory
frame.add{type="label", caption=request.inventory, style=NAME.style.inventory_number_label}
-- Calculate amount missing
local diff = request.count - request.inventory
-- Show one-time request logistic button
if diff > 0 then
local enabled, style, tooltip = make_request_button_properties(request,
playerdata.logistic_requests[request.name])
frame.add{
type="button",
caption=diff,
enabled=enabled,
style=style,
tooltip=tooltip,
tags={ghost_counter_request=request.name}
}
else -- Show request fulfilled sprite
local sprite_container = frame.add{
type="flow",
direction="horizontal",
style=NAME.style.ghost_request_fulfilled_flow
}
sprite_container.add{
type="sprite",
sprite="utility/check_mark_white",
resize_to_sprite=false,
style=NAME.style.ghost_request_fulfilled_sprite
}
end
-- Hide frame if ghost count is 0 and player toggled hide empty requests
frame.visible = request.count > 0 or not playerdata.options.hide_empty_requests and true or
false
end
---Event handler for GUI button clicks
---@param event EventData.on_gui_click Event table
function Gui.on_gui_click(event)
local player_index = event.player_index
local element = event.element
local element_name = element.name
if element.name == NAME.gui.close_button then
-- Close button
Gui.toggle(player_index, false)
elseif element.tags and element.tags.ghost_counter_request then
-- One-time logistic request/craft button
local playerdata = get_make_playerdata(player_index)
local request_name = element.tags.ghost_counter_request --[[@as string]]
if event.shift == true then
local request = playerdata.job.requests[request_name]
if request then
local result, crafted = craft_request(event.player_index, request)
local player = game.get_player(player_index) --[[@as LuaPlayer]]
if result == "no-crafts-needed" then
player.create_local_flying_text{
text={"ghost-counter-message.crafts-not-needed"},
create_at_cursor=true
}
elseif result == "attempted" and crafted == 0 then
player.create_local_flying_text{
text={"ghost-counter-message.crafts-attempted-none"},
create_at_cursor=true
}
end
end
else
if not playerdata.logistic_requests[request_name] then
make_one_time_logistic_request(player_index, request_name)
Gui.update_list(player_index)
else
restore_prior_logistic_request(player_index, request_name)
Gui.update_list(player_index)
end
end
elseif element_name == NAME.gui.hide_empty_button then
local playerdata = get_make_playerdata(player_index)
local new_state = not playerdata.options.hide_empty_requests
playerdata.options.hide_empty_requests = new_state
element.style = new_state and NAME.style.titlebar_button_active or
NAME.style.titlebar_button
element.sprite = new_state and NAME.sprite.hide_empty_black or NAME.sprite.hide_empty_white
element.clicked_sprite = new_state and NAME.sprite.hide_empty_white or
NAME.sprite.hide_empty_black
Gui.update_list(player_index)
elseif element_name == NAME.gui.get_signals_button then
make_combinators_blueprint(event.player_index)
elseif element_name == NAME.gui.craft_all_button then
local playerdata = get_make_playerdata(player_index)
for _, request in pairs(playerdata.job.requests) do
craft_request(player_index, request)
end
elseif element_name == NAME.gui.request_all_button then
local playerdata = get_make_playerdata(player_index)
for _, request in pairs(playerdata.job.requests) do
if request.count > 0 and not playerdata.logistic_requests[request.name] then
make_one_time_logistic_request(player_index, request.name)
end
end
Gui.update_list(player_index)
elseif element_name == NAME.gui.cancel_all_button then
cancel_all_one_time_requests(player_index)
Gui.update_list(player_index)
end
end
script.on_event(defines.events.on_gui_click, Gui.on_gui_click)