mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 23:04:52 +09:00
Replace unsupported minimap:player_session_start IPC with existing minimap:player_position channel to emit immediate session start on join.
591 lines
16 KiB
Lua
591 lines
16 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
|
|
recipe = prev.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 }
|
|
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
|