local Entity = require("__stdlib__/stdlib/entity/entity") local Direction = require("__stdlib__/stdlib/area/direction") local Position = require("__stdlib__/stdlib/area/position") local Area = require("__stdlib__/stdlib/area/area") local H = require("scripts.helpers") local C = require("common.constants") -- local settings = require("scripts.settings") local M = {} ---@class DispatcherTrainStopData ---@field entity LuaEntity ---@field input LuaEntity ---Gets the data attached to a dispatcher ---@param entity LuaEntity ---@return DispatcherTrainStopData|nil ---@nodiscard local function get_dispatcher_data(entity) if not global.tws_dispatchers then return nil end return global.tws_dispatchers[entity.unit_number] end ---Attaches some data to a dispatcher ---@param entity LuaEntity ---@param value DispatcherTrainStopData|nil local function set_dispatcher_data(entity, value) if not global.tws_dispatchers then global.tws_dispatchers = {} end global.tws_dispatchers[entity.unit_number] = value end -- {{{ CreateStop ---Runs when a stop gets created ---@param entity LuaEntity local function CreateStop(entity) if get_dispatcher_data(entity) then -- TODO: better logs game.print("Duplicate unit number, wtf") return end local stop_offset = C.stop_offsets[entity.name] local offset_adjustment = (not stop_offset) and stop_offset or Direction.to_vector(Direction.next(entity.direction), stop_offset) -- local offset_adjustment = 0 ---@type MapPosition local input_position = Position(entity.position) + offset_adjustment -- Moves the center we rotate around to the -- center of the top-left block, -- Position(0.5, 0.5) + -- then rotates counterclockwise once, -- and go 0.5 more in that direction -- Direction.to_vector( -- Direction.next(entity.direction, true), 0.5) + Direction.to_vector(entity.direction, 0.5) local search_area = Area.shrink(Area.new({ input_position }), 0.001) local input -- {{{ Handle blueprint ghosts and existing IO entities preserving circuit connections local ghosts = entity.surface.find_entities(search_area) for _, ghost in pairs(ghosts) do if ghost.valid then if ghost.name == "entity-ghost" then if ghost.ghost_name == C.input_item_name then _, input = ghost.revive() end -- something has built I/O already (e.g.) Creative Mode Instant Blueprint elseif ghost.name == C.input_item_name then input = ghost end end end -- }}} if input == nil then -- create new input = entity.surface.create_entity({ name = C.input_item_name, position = input_position, force = entity.force, }) end if input == nil then -- TODO: logging game.print("Something went wrong") return end input.operable = false -- disable gui input.minable = false input.destructible = false -- don't bother checking if alive ---@type DispatcherTrainStopData local stop_data = { entity = entity, input = input } set_dispatcher_data(entity, stop_data) end -- }}} -- {{{ OnEntityCreated local function OnEntityCreated(event) local entity = event.created_entity or event.entity or event.destination if not entity or not entity.valid then return end if entity.name == C.item_name then CreateStop(entity) end end -- }}} -- {{{ RemoveStop ---Runs once a train stop has been removed. ---@param entity LuaEntity ---@param create_ghosts boolean function RemoveStop(entity, create_ghosts) local stop = get_dispatcher_data(entity) -- {{{ Destroy io entities if stop then ---@type LuaEntity local input = stop.input if input and input.valid then if create_ghosts then input.destructable = true input.die() else input.destroy() end end end -- }}} -- Delete entity data set_dispatcher_data(entity, nil) end -- }}} -- {{{ OnEntityRemoved ---Runs when any kind of entity has been removed. ---@param event EventData.on_entity_died|EventData.script_raised_destroy|EventData.on_robot_pre_mined|EventData.on_pre_player_mined_item ---@param create_ghosts any function OnEntityRemoved(event, create_ghosts) local entity = event.entity if not entity or not entity.valid then return end if entity.name == C.item_name then RemoveStop(entity, create_ghosts) end end -- }}} -- {{{ Remove entity data when surfaces get removed. ---@param event EventData.on_pre_surface_deleted|EventData.on_pre_surface_cleared function OnSurfaceRemoved(event) -- stop references local surfaceID = event.surface_index local surface = game.surfaces[surfaceID] if surface then local train_stops = surface.find_entities_filtered({ type = "train-stop" }) for _, entity in pairs(train_stops) do if entity.name == C.item_name then RemoveStop(entity, false) end end end end -- }}} -- {{{ UpdateSchedule ---Runs every tick to update train schedules ---@param stop DispatcherTrainStopData function UpdateSchedule(stop) local control = stop.input.get_or_create_control_behavior() if not control or not control.valid then return end local signals = { action_create = { type = "virtual", name = "signal-C" }, action_jump = { type = "virtual", name = "signal-J" }, target_after = { type = "virtual", name = "signal-A" }, target_before = { type = "virtual", name = "signal-B" }, template_past = { type = "virtual", name = "signal-P" }, template_future = { type = "virtual", name = "signal-F" }, option_infinite = { type = "virtual", name = "signal-O" }, option_extend = { type = "virtual", name = "signal-E" }, } local create = H.get_signal_count(control, signals.action_create) local jump = H.get_signal_count(control, signals.action_jump) local train = stop.entity.get_stopped_train() if not train or not train.valid then return end local schedule = train.schedule if not schedule then return end schedule = H.copy(schedule) if create > 0 then local target_after = H.get_signal_count(control, signals.target_after) local target_before = H.get_signal_count(control, signals.target_before) local target_index = schedule.current + target_after - target_before local template_future = H.get_signal_count(control, signals.template_future) local template_past = H.get_signal_count(control, signals.template_past) local template_index = schedule.current + template_future - template_past local template = H.get_cycled(schedule.records, template_index) local copy = H.copy(template) local is_temporary = 0 == H.get_signal_count(control, signals.option_infinite) copy.temporary = is_temporary local extend = H.get_signal_count(control, signals.option_extend) if extend > 0 and copy.station then copy.station = copy.station .. " " .. tostring(extend) end H.list_insert(schedule.records, target_index, copy) train.schedule = schedule end if jump > 0 and #schedule.records > 0 then schedule.current = (schedule.current + jump - 1) % #schedule.records + 1 train.schedule = schedule end end -- }}} -- {{{ OnTick function OnTick() for _, v in pairs(global.tws_dispatchers or {}) do UpdateSchedule(v) end end -- }}} function M.setup() local filters_on_built = { { filter = "type", type = "train-stop" } } local filters_on_mined = { { filter = "type", type = "train-stop" } } -- {{{ On create events local on_create_events = { defines.events.on_built_entity, defines.events.on_robot_built_entity, defines.events.script_raised_built, defines.events.script_raised_revive, defines.events.on_entity_cloned, } for _, event in pairs(on_create_events) do script.on_event(event, OnEntityCreated, filters_on_built) end -- }}} -- {{{ On remove events local on_remove_events = { defines.events.on_pre_player_mined_item, defines.events.on_robot_pre_mined, defines.events.script_raised_destroy, } for _, event in pairs(on_remove_events) do script.on_event(event, OnEntityRemoved, filters_on_mined) end script.on_event(defines.events.on_entity_died, function(event) OnEntityRemoved(event, true) end, filters_on_mined) -- }}} -- {{{ On surface removed script.on_event({ defines.events.on_pre_surface_deleted, defines.events.on_pre_surface_cleared, }, OnSurfaceRemoved) -- }}} script.on_event(defines.events.on_tick, OnTick) end return M