mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
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.
592 lines
17 KiB
Lua
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
|