Files
Danielv123 5ddf3fe34f fix(minimap): restore nested previous_recipe name access
Furnace previous_recipe ids are nested under prev.name.name in this context. Restore the original access path and leave an inline maintainer note to prevent repeat regressions.
2026-02-21 19:49:59 +01:00

592 lines
17 KiB
Lua

local clusterio_api = require("modules/clusterio/api")
local compat = require("modules/clusterio/compat")
-- Hot-loop local references for performance
local math_floor = math.floor
local bit32_rshift = bit32.rshift
local table_insert = table.insert
local minimap = {}
-- Convert RGB565 to RGB888 values
local function rgb565_to_rgb888(rgb565_value)
local r = math_floor(bit32_rshift(rgb565_value, 11) * 255 / 31)
local g = math_floor(bit32_rshift(bit32.band(rgb565_value, 0x07E0), 5) * 255 / 63)
local b = math_floor(bit32.band(rgb565_value, 0x001F) * 255 / 31)
return r, g, b
end
-- Queue a chunk for update
local function queue_chunk_for_update(chunk_position)
if not storage.minimap or not storage.minimap.enabled then
return
end
-- If the chunk isn't already in the queue, add it
for _, queued_chunk in pairs(storage.minimap.chunk_update_queue) do
if queued_chunk.x == chunk_position.x and queued_chunk.y == chunk_position.y then
return
end
end
table.insert(storage.minimap.chunk_update_queue, chunk_position)
end
-- Send chart tag data to plugin
local function send_chart_tag_data(tag_data)
clusterio_api.send_json("minimap:chart_tag_data", tag_data)
end
-- Send recipe data to plugin
local function send_recipe_data(recipe_data)
clusterio_api.send_json("minimap:recipe_data", recipe_data)
end
-- Send player position data to plugin
local function send_player_position_data(player_data)
clusterio_api.send_json("minimap:player_position", player_data)
end
-- Send player session end data to plugin
local function send_player_session_end(session_data)
clusterio_api.send_json("minimap:player_session_end", session_data)
end
-- Check if a position has moved significantly (≥1 tile)
local function has_moved_significantly(old_pos, new_pos)
if not old_pos then
return true
end
local dx = math.abs(new_pos.x - old_pos.x)
local dy = math.abs(new_pos.y - old_pos.y)
return dx >= 1 or dy >= 1
end
-- Process and send player position updates
local function process_player_positions()
if not storage.minimap or not storage.minimap.enabled then
return
end
for _, player in pairs(game.connected_players) do
if player.valid and player.character and player.character.valid then
-- Use physical controller position for accurate tracking when in vehicles/remotes
local surface = player.physical_surface
local position = player.physical_position
local player_key = player.index
-- Initialize player tracking data if needed
if not storage.minimap.player_positions then
storage.minimap.player_positions = {}
end
local last_position = storage.minimap.player_positions[player_key]
local current_sec = math_floor(game.tick / 60)
-- Check if position changed significantly or timeout elapsed
local should_update = false
if not last_position then
-- First position for this player
should_update = true
elseif has_moved_significantly(last_position.position, position) then
should_update = true
elseif (current_sec - last_position.last_update_sec) >= 5 then
-- Send update every 5 seconds even if not moving (for timeout)
should_update = true
end
if should_update then
local player_data = {
player_name = player.name,
surface = surface.name,
x = position.x,
y = position.y,
sec = current_sec,
}
send_player_position_data(player_data)
-- Update tracking data
storage.minimap.player_positions[player_key] = {
position = { x = position.x, y = position.y },
last_update_sec = current_sec,
}
end
end
end
end
-- Handle player session start
local function on_player_joined_game(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
local player = game.get_player(event.player_index)
if not player or not player.valid then
return
end
-- Emit session start immediately when player joins using existing player_position channel
local surface = player.physical_surface or player.surface
local position = player.physical_position or player.position
local player_data = {
player_name = player.name,
surface = surface.name,
x = position.x,
y = position.y,
sec = math_floor(game.tick / 60),
}
send_player_position_data(player_data)
-- Clear any existing position data for this player to ensure fresh tracking
if storage.minimap.player_positions then
storage.minimap.player_positions[player.index] = nil
end
end
-- Handle player session end
local function on_player_left_game(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
local player = game.get_player(event.player_index)
if not player or not player.valid then
return
end
send_player_session_end({
player_name = player.name,
surface = player.surface.name,
})
-- Clean up position tracking data
if storage.minimap.player_positions then
storage.minimap.player_positions[event.player_index] = nil
end
end
-- Helper for creating and sending recipe start/stop events
local function update_entity_recipe(entity)
if not entity or not entity.valid then
return
end
if not storage.minimap or not storage.minimap.enabled then
return
end
-- Only consider entities that can have recipes
local recipe = nil
if entity.type == "assembling-machine" then
local rec_obj = entity.get_recipe()
if rec_obj then
recipe = rec_obj.name
end
elseif entity.type == "furnace" then
local prev = entity.previous_recipe
if prev and prev.name then
-- Factorio furnaces expose previous_recipe with the id nested at previous_recipe.name.name.
recipe = prev.name.name
end
end
local cache = storage.minimap.recipe_cache
local unit_number = entity.unit_number
if not unit_number then
return -- Cannot track entities without persistent ID
end
local previous = cache[unit_number]
if previous and previous.recipe_name == recipe then
return -- No change
end
local position = entity.position
local data_common = {
position = { position.x, position.y },
surface = entity.surface.name,
force = entity.force.name,
}
-- Close previous recipe if it existed
if previous and previous.recipe_name then
local end_data = {
position = data_common.position,
surface = data_common.surface,
force = data_common.force,
tick = game.tick,
recipe = nil,
}
send_recipe_data(end_data)
end
-- Open new recipe interval if recipe now set
if recipe then
local icon_signal = nil
local recipe_proto = prototypes.recipe[recipe]
if recipe_proto and recipe_proto.main_product then
icon_signal = { type = recipe_proto.main_product.type, name = recipe_proto.main_product.name }
end
local start_data = {
position = data_common.position,
surface = data_common.surface,
force = data_common.force,
tick = game.tick,
recipe = recipe,
icon = icon_signal,
}
send_recipe_data(start_data)
cache[unit_number] = { recipe_name = recipe }
else
-- Recipe cleared
cache[unit_number] = nil
end
end
-- Create tag data structure
local function create_tag_data(tag, start_tick, end_tick)
return {
tag_number = tag.tag_number,
start_tick = start_tick,
end_tick = end_tick,
force = tag.force.name,
surface = tag.surface.name,
position = {tag.position.x, tag.position.y},
text = tag.text,
icon = tag.icon or nil,
last_user = tag.last_user and tag.last_user.name or nil
}
end
local function dump_chunk_chart(chunk_position)
local data = {}
local surfaces = game.surfaces
local forces = game.forces
for _, force in pairs(forces) do
for _, surface in pairs(surfaces) do
local chart_data = force.get_chunk_chart(surface, chunk_position)
if chart_data then
-- Use Factorio's built-in deflate compression and base64 encoding
local encoded_data = helpers.encode_string(chart_data)
table_insert(data, {surface = surface.name, force = force.name, chart_data = encoded_data})
end
end
end
-- Calculate world position from chunk position
local world_x = chunk_position.x * 32
local world_y = chunk_position.y * 32
-- Send tile data to the plugin
clusterio_api.send_json("minimap:tile_data", {
type = "chart",
position = {world_x, world_y},
tick = game.tick,
data = data,
})
end
-- Chart tag event handlers
local function on_chart_tag_added(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
local tag = event.tag
if not tag or not tag.valid then
return
end
local start_tick = game.tick
local tag_data = create_tag_data(tag, start_tick, nil)
-- Send to plugin
send_chart_tag_data(tag_data)
end
local function on_chart_tag_modified(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
local tag = event.tag
if not tag or not tag.valid then
return
end
-- End the old version at current tick
local end_tick = game.tick
local old_tag_data = create_tag_data(tag, nil, end_tick)
send_chart_tag_data(old_tag_data)
-- Create new tag entry starting at next tick
local start_tick = game.tick + 1
local tag_data = create_tag_data(tag, start_tick, nil)
-- Send to plugin
send_chart_tag_data(tag_data)
end
local function on_chart_tag_removed(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
local tag = event.tag
if not tag or not tag.valid then
return
end
-- End the tag
local end_tick = game.tick
local tag_data = create_tag_data(tag, nil, end_tick)
send_chart_tag_data(tag_data)
end
-- Event handlers
local function on_chunk_charted(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
-- Convert area to chunk position
local chunk_x = math_floor(event.area.left_top.x / 32)
local chunk_y = math_floor(event.area.left_top.y / 32)
queue_chunk_for_update({x = chunk_x, y = chunk_y})
end
local function on_entity_built(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
if event.created_entity and event.created_entity.valid then
local entity = event.created_entity
local position = entity.position
local chunk_x = math_floor(position.x / 32)
local chunk_y = math_floor(position.y / 32)
queue_chunk_for_update({x = chunk_x, y = chunk_y})
-- Track recipe for newly built crafting entities
update_entity_recipe(entity)
end
end
local function on_entity_removed(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
if event.entity and event.entity.position then
local entity = event.entity
local position = entity.position
local chunk_x = math_floor(position.x / 32)
local chunk_y = math_floor(position.y / 32)
queue_chunk_for_update({x = chunk_x, y = chunk_y})
-- End recipe interval for removed entity
update_entity_recipe(entity)
end
end
local function on_tile_changed(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
-- Get unique chunks that were affected
local affected_chunks = {}
for _, tile in pairs(event.tiles) do
local chunk_x = math_floor(tile.position.x / 32)
local chunk_y = math_floor(tile.position.y / 32)
local chunk_key = chunk_x .. "," .. chunk_y
if not affected_chunks[chunk_key] then
affected_chunks[chunk_key] = {x = chunk_x, y = chunk_y}
end
end
-- Update each affected chunk
for _, chunk_pos in pairs(affected_chunks) do
queue_chunk_for_update(chunk_pos)
end
end
-- Handle recipe change via settings paste
local function on_entity_settings_pasted(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
if event.destination and event.destination.valid then
update_entity_recipe(event.destination)
end
end
local function on_gui_closed(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
-- We're only interested in entity GUIs
if event.gui_type ~= defines.gui_type.entity then
return
end
local entity = event.entity
if entity and entity.valid and (entity.type == "assembling-machine" or entity.type == "furnace") then
update_entity_recipe(entity)
end
end
-- Initialize the module
local function init()
if not storage.minimap then
storage.minimap = {
enabled = true,
chunk_update_queue = {},
recipe_cache = {},
player_positions = {}
}
end
if not storage.minimap.enabled then
return
end
-- Ensure all required fields exist
if not storage.minimap.chunk_update_queue then
storage.minimap.chunk_update_queue = {}
end
if not storage.minimap.recipe_cache then
storage.minimap.recipe_cache = {}
end
if not storage.minimap.player_positions then
storage.minimap.player_positions = {}
end
-- Clear chunk queue before rebuilding
storage.minimap.chunk_update_queue = {}
-- Queue all existing charted chunks for update
local chunks_queued = 0
for _, force in pairs(game.forces) do
for _, surface in pairs(game.surfaces) do
-- Iterate through all generated chunks on this surface
for chunk in surface.get_chunks() do
local chunk_position = {x = chunk.x, y = chunk.y}
-- Check if this chunk is charted for this force
if force.is_chunk_charted(surface, chunk_position) then
table_insert(storage.minimap.chunk_update_queue, chunk_position)
chunks_queued = chunks_queued + 1
end
end
-- Initialize existing chart tags
local existing_tags = force.find_chart_tags(surface)
for _, tag in pairs(existing_tags) do
local start_tick = game.tick
local tag_data = create_tag_data(tag, start_tick, nil)
-- Send to plugin
send_chart_tag_data(tag_data)
end
-- Initialize existing crafting recipes on this surface and force
local crafting_entities = surface.find_entities_filtered{
type = {
"assembling-machine",
"furnace",
}
}
for _, entity in pairs(crafting_entities) do
if entity.valid and entity.force == force then
local recipe
if entity.type == "assembling-machine" then
recipe = entity.get_recipe()
elseif entity.type == "furnace" then
local prev = entity.previous_recipe
if prev and prev.name then
recipe = { name = prev.name.name }
end
end
if recipe then
local recipe_proto = prototypes.recipe[recipe.name]
local icon_signal = nil
if recipe_proto and recipe_proto.main_product then
icon_signal = { type = recipe_proto.main_product.type, name = recipe_proto.main_product.name }
end
local data = {
position = { entity.position.x, entity.position.y },
surface = surface.name,
force = force.name,
tick = game.tick,
recipe = recipe.name,
icon = icon_signal,
}
send_recipe_data(data)
-- Cache for tracking
storage.minimap.recipe_cache[entity.unit_number] = { recipe_name = recipe.name }
end
end
end
end
end
end
local function on_tick(event)
if not storage.minimap or not storage.minimap.enabled then
return
end
-- Process the chunk update queue
if #storage.minimap.chunk_update_queue > 0 then
local chunk_position = table.remove(storage.minimap.chunk_update_queue, 1)
dump_chunk_chart(chunk_position)
end
-- Process player position updates every 250ms (15 ticks at 60 UPS)
if game.tick % 15 == 0 then
process_player_positions()
end
end
-- Export functions
minimap.dump_chunk_chart = dump_chunk_chart
minimap.events = {}
-- Ensure this module only loads on Factorio 2.0 or later
if not compat.version_ge("2.0") then
log("[minimap] Minimap plugin disabled - requires Factorio 2.0 or later, current version: " .. (script.active_mods and script.active_mods.base or "unknown"))
else
minimap.events[clusterio_api.events.on_server_startup] = init
minimap.events[defines.events.on_tick] = on_tick
minimap.events[defines.events.on_chunk_charted] = on_chunk_charted
minimap.events[defines.events.on_built_entity] = on_entity_built
minimap.events[defines.events.on_robot_built_entity] = on_entity_built
minimap.events[defines.events.on_entity_died] = on_entity_removed
minimap.events[defines.events.on_player_mined_entity] = on_entity_removed
minimap.events[defines.events.on_robot_mined_entity] = on_entity_removed
minimap.events[defines.events.on_player_built_tile] = on_tile_changed
minimap.events[defines.events.on_robot_built_tile] = on_tile_changed
minimap.events[defines.events.on_player_mined_tile] = on_tile_changed
minimap.events[defines.events.on_robot_mined_tile] = on_tile_changed
minimap.events[defines.events.script_raised_set_tiles] = on_tile_changed
minimap.events[defines.events.on_chart_tag_added] = on_chart_tag_added
minimap.events[defines.events.on_chart_tag_modified] = on_chart_tag_modified
minimap.events[defines.events.on_chart_tag_removed] = on_chart_tag_removed
minimap.events[defines.events.on_entity_settings_pasted] = on_entity_settings_pasted
minimap.events[defines.events.on_gui_closed] = on_gui_closed
minimap.events[defines.events.on_player_joined_game] = on_player_joined_game
minimap.events[defines.events.on_player_left_game] = on_player_left_game
end
return minimap