commit 62e12a520464bb0cec42dfb174eb174ac631346a Author: Matei Adriel Date: Sat Dec 31 19:29:58 2022 +0100 Base functionality has been implemented diff --git a/lua/prescient-trains/.neoconf.json b/lua/prescient-trains/.neoconf.json new file mode 100644 index 0000000..99e3e78 --- /dev/null +++ b/lua/prescient-trains/.neoconf.json @@ -0,0 +1,33 @@ +{ + "lspconfig": { + "sumneko_lua": { + "Lua.workspace.library": [ + "/home/adrielus/Projects/Foreign/vscode-factoriomod-debug/sumneko-3rd/factorio/library", + "/home/adrielus/.steam/steam/steamapps/common/Factorio/data" + ], + "Lua.runtime.plugin": "/home/adrielus/Projects/Foreign/vscode-factoriomod-debug/sumneko-3rd/factorio/plugin.lua", + "Lua.workspace.checkThirdParty": false, + "Lua.runtime.version": "Lua 5.2", + "Lua.runtime.special": { + "__object_name": "type" + }, + "Lua.runtime.builtin": { + "io": "disable", + "os": "disable", + "coroutine": "disable", + "package": "disable", + "math": "disable", + "debug": "disable" + }, + "Lua.diagnostics.globals": [ + "__DebugAdapter", + "__Profiler" + ] + } + }, + "neodev": { + "library": { + "enabled": false + } + } +} diff --git a/lua/prescient-trains/common/constants.lua b/lua/prescient-trains/common/constants.lua new file mode 100644 index 0000000..e39cf4d --- /dev/null +++ b/lua/prescient-trains/common/constants.lua @@ -0,0 +1,11 @@ +local M = {} + +M.item_name = "dispatcher-train-stop" +M.input_item_name = M.item_name .. "-input" + +-- Distance from rails for each stop type. +-- Useful if, in the future, I decide to +-- add support for cargo ships +M.stop_offsets = { [M.item_name] = 0 } + +return M diff --git a/lua/prescient-trains/control.lua b/lua/prescient-trains/control.lua new file mode 100644 index 0000000..3bd3b8e --- /dev/null +++ b/lua/prescient-trains/control.lua @@ -0,0 +1 @@ +require("scripts.train_stop_events").setup() diff --git a/lua/prescient-trains/data.lua b/lua/prescient-trains/data.lua new file mode 100644 index 0000000..3837f71 --- /dev/null +++ b/lua/prescient-trains/data.lua @@ -0,0 +1,4 @@ +require("prototypes.technologies") +require("prototypes.recipes") +require("prototypes.items") +require("prototypes.entities") diff --git a/lua/prescient-trains/info.json b/lua/prescient-trains/info.json new file mode 100644 index 0000000..d56a08c --- /dev/null +++ b/lua/prescient-trains/info.json @@ -0,0 +1,9 @@ +{ + "name": "tinker-with-schedules", + "version": "0.1.0", + "title": "Tinker with schedules", + "author": "Adriel", + "factorio_version": "1.1", + "dependencies": ["base >= 1.1", "stdlib >= 1.4"], + "description": "Let's you use combinators to add temporary stops to train schedules" +} diff --git a/lua/prescient-trains/locale/en/en.cfg b/lua/prescient-trains/locale/en/en.cfg new file mode 100644 index 0000000..76c2708 --- /dev/null +++ b/lua/prescient-trains/locale/en/en.cfg @@ -0,0 +1,25 @@ +[mod-setting-name] + +[mod-setting-description] + +[entity-name] +dispatcher-train-stop=Dispatcher +dispatcher-train-stop-input=Dispatcher input + +[entity-description] +dispatcher-train-stop=Allows you to add temporary stops to train schedules using signals +dispatcher-train-stop-input=Listens for signals coming from the exterior + +[item-name] +dispatcher-train-stop=Dispatcher +dispatcher-train-stop-input=Dispatcher input + +[item-description] +dispatcher-train-stop=Allows you to add temporary stops to train schedules using signals +dispatcher-train-stop-input=Uh, why is this here + +[technology-name] +tinker-with-schedules=Tinker with schedules + +[technology-description] +tinker-with-schedules=Allows you to add temporary stops to train schedules using signals diff --git a/lua/prescient-trains/prototypes/entities.lua b/lua/prescient-trains/prototypes/entities.lua new file mode 100644 index 0000000..ebacf1b --- /dev/null +++ b/lua/prescient-trains/prototypes/entities.lua @@ -0,0 +1,44 @@ +local C = require("common.constants") + +local function copy(from, name) + local copied = table.deepcopy(from) + copied.name = name + copied.minable.result = name + + return copied +end + +-- Train stop +-- {{{ +local dispatcher_train_stop = copy(data.raw["train-stop"]["train-stop"], + C.item_name) + +dispatcher_train_stop.color = {r = 0.46, g = 0.01, b = 0.98, a = 1} +dispatcher_train_stop.next_upgrade = nil +dispatcher_train_stop.selection_box = {{-0.9, -0.6}, {0.9, 0.9}} +-- }}} + +-- Input +-- {{{ +local dispatcher_train_stop_in = copy(data.raw["lamp"]["small-lamp"], + C.input_item_name) + +dispatcher_train_stop_in.minable = nil +dispatcher_train_stop_in.next_upgrade = nil + +dispatcher_train_stop_in.selection_box = {{-0.5, -0.5}, {0.5, 0.5}} +dispatcher_train_stop_in.selection_priority = + (dispatcher_train_stop_in.selection_priority or 50) + 10 + +dispatcher_train_stop_in.collision_box = {{-0.15, -0.15}, {0.15, 0.15}} +dispatcher_train_stop_in.collision_mask = {"rail-layer"} -- collide only with rail entities + +dispatcher_train_stop_in.light = {intensity = 1, size = 6} +dispatcher_train_stop_in.energy_source = {type = "void"} +dispatcher_train_stop_in.energy_usage_per_tick = "10W" +dispatcher_train_stop_in.selectable_in_game = true + +dispatcher_train_stop_in.flags = {"placeable-off-grid", "player-creation"} +-- }}} + +data:extend({dispatcher_train_stop, dispatcher_train_stop_in}) diff --git a/lua/prescient-trains/prototypes/items.lua b/lua/prescient-trains/prototypes/items.lua new file mode 100644 index 0000000..d79fd31 --- /dev/null +++ b/lua/prescient-trains/prototypes/items.lua @@ -0,0 +1,24 @@ +local C = require("common.constants") + +local item_dispatcher_train_stop = + table.deepcopy(data.raw["item"]["train-stop"]) + +item_dispatcher_train_stop.name = C.item_name +item_dispatcher_train_stop.order = item_dispatcher_train_stop.order .. "-c" +item_dispatcher_train_stop.icon_size = 64 +item_dispatcher_train_stop.place_result = C.item_name +item_dispatcher_train_stop.icons = { + { + icon = "__base__/graphics/icons/train-stop.png", + tint = {r = 0.46, g = 0.01, b = 0.98, a = 0.1} + } +} + +local item_dispatcher_train_stop_in = table.deepcopy( + data.raw["item"]["small-lamp"]) + +item_dispatcher_train_stop_in.name = C.input_item_name +item_dispatcher_train_stop_in.place_result = C.input_item_name +item_dispatcher_train_stop_in.flags = {"hidden"} + +data:extend({item_dispatcher_train_stop, item_dispatcher_train_stop_in}) diff --git a/lua/prescient-trains/prototypes/recipes.lua b/lua/prescient-trains/prototypes/recipes.lua new file mode 100644 index 0000000..9ae748d --- /dev/null +++ b/lua/prescient-trains/prototypes/recipes.lua @@ -0,0 +1,14 @@ +local C = require("common.constants") + +data:extend({ + { + type = "recipe", + name = C.item_name, + energy_required = 5, + enabled = false, + ingredients = { + {"train-stop", 1}, {"electronic-circuit", 20}, {"copper-cable", 20} + }, + result = C.item_name + } +}) diff --git a/lua/prescient-trains/prototypes/technologies.lua b/lua/prescient-trains/prototypes/technologies.lua new file mode 100644 index 0000000..00da358 --- /dev/null +++ b/lua/prescient-trains/prototypes/technologies.lua @@ -0,0 +1,20 @@ +local C = require("common.constants") + +data:extend({ + { + type = "technology", + name = "tinker-with-schedules", + icon_size = 256, + icon = "__base__/graphics/technology/automated-rail-transportation.png", + effects = { { type = "unlock-recipe", recipe = C.item_name } }, + prerequisites = { "automated-rail-transportation", "circuit-network" }, + unit = { + count = 100, + ingredients = { + { "automation-science-pack", 1 }, { "logistic-science-pack", 1 } + }, + time = 15 + }, + order = "c-g-aa" + } +}) diff --git a/lua/prescient-trains/scripts/helpers.lua b/lua/prescient-trains/scripts/helpers.lua new file mode 100644 index 0000000..986bf62 --- /dev/null +++ b/lua/prescient-trains/scripts/helpers.lua @@ -0,0 +1,72 @@ +local M = {} + +-- See [this original implementation](https://github.com/coltonj96/UsefulCombinators/blob/master/UsefulCombinators_0.4.4/control.lua#L2062) +---Get the total count of some signal in some control behavior. +---@param control LuaControlBehavior +---@param signal SignalID +function M.get_signal_count(control, signal) + if not signal then + return 0 + end + + local red = control.get_circuit_network(defines.wire_type.red) + local green = control.get_circuit_network(defines.wire_type.green) + + local total = 0 + + if red then + total = total + (red.get_signal(signal) or 0) + end + + if green then + total = total + (green.get_signal(signal) or 0) + end + + return total +end + +---Returns the index if going past #array or under 1 would loop back the other side. +---Assumes the array is nonempty +---@generic T +---@param array T[] +---@param index integer +---@returns integer +---@nodiscard +function M.mod_index(array, index) + return (index - 1) % #array + 1 +end + +---Uses the above function to normalize the index between the start and end of the array. +---Assumes the array is nonempty +---@generic T +---@param array T[] +---@param index integer +---@return T +---@nodiscard +function M.get_cycled(array, index) + return array[M.mod_index(array, index)] +end + +---Annotated version of table.inset +---@generic T +---@param list T[] +---@param at integer +---@param value T +function M.list_insert(list, at, value) + table.insert(list,at, value) +end + +---Shallow copies some table +---@generic T :table +---@param object T +---@return T +function M.copy(object) + local result = {} + + for k, v in pairs(object) do + result[k] = v end + + return result +end + +return M diff --git a/lua/prescient-trains/scripts/settings.lua b/lua/prescient-trains/scripts/settings.lua new file mode 100644 index 0000000..18c7a79 --- /dev/null +++ b/lua/prescient-trains/scripts/settings.lua @@ -0,0 +1,6 @@ +local M = {} + +M.message_level = tonumber(settings.global["tws-interface-console-level"].value) +M.debug_log = settings.global["tws-interface-debug-logfile"].value + +return M diff --git a/lua/prescient-trains/scripts/train_stop_events.lua b/lua/prescient-trains/scripts/train_stop_events.lua new file mode 100644 index 0000000..ca90ac8 --- /dev/null +++ b/lua/prescient-trains/scripts/train_stop_events.lua @@ -0,0 +1,313 @@ +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 diff --git a/lua/prescient-trains/stylua.toml b/lua/prescient-trains/stylua.toml new file mode 100644 index 0000000..03d2f90 --- /dev/null +++ b/lua/prescient-trains/stylua.toml @@ -0,0 +1,3 @@ +column_width = 80 +indent_width = 2 +indent_type = "Spaces"