diff --git a/plugins/research_sync/module/sync.lua b/plugins/research_sync/module/sync.lua index 23bf6301..045e81ce 100644 --- a/plugins/research_sync/module/sync.lua +++ b/plugins/research_sync/module/sync.lua @@ -38,6 +38,10 @@ end --- @class ResearchSyncData --- @field technologies table --- @field ignore_research_finished boolean +--- @field follow boolean? nil until first set; nil counts as following +--- @field ignore_queue_events boolean +--- @field queue_dirty boolean +--- @field queue_player string? --- @param no_early_return boolean? --- @return ResearchSyncData @@ -53,6 +57,12 @@ local function get_script_data(no_early_return) if research_sync.ignore_research_finished == nil then research_sync.ignore_research_finished = false end + if research_sync.ignore_queue_events == nil then + research_sync.ignore_queue_events = false + end + if research_sync.queue_dirty == nil then + research_sync.queue_dirty = false + end local force = game.forces["player"] for _, tech in pairs(force.technologies) do @@ -143,6 +153,117 @@ sync.on_nth_tick[79] = function(event) end end +--- @param force LuaForce +--- @return string[] +local function queue_names(force) + local names = {} + for _, tech in pairs(force.research_queue) do + table.insert(names, tech.name) + end + return names +end + +--- @param force LuaForce +--- @param names string[] +local function write_queue(force, names) + local script_data = get_script_data() + script_data.ignore_queue_events = true + local ok, err = pcall(function() force.research_queue = names end) + script_data.ignore_queue_events = false + if not ok then + log("research_sync: failed to write research queue: " .. tostring(err)) + end +end + +--- @param names string[] +--- @return string +local function format_queue(names) + if #names == 0 then + return "(empty)" + end + local tags = {} + for _, name in ipairs(names) do + table.insert(tags, "[technology=" .. name .. "]") + end + return table.concat(tags, " ") +end + +--- @param player LuaPlayer +local function ensure_gui(player) + local frame = player.gui.relative["research_sync_queue"] + if not frame then + frame = player.gui.relative.add { + type = "frame", + name = "research_sync_queue", + caption = "Research sync", + anchor = { + gui = defines.relative_gui_type.lab_gui, + position = defines.relative_gui_position.right, + }, + } + frame.add { + type = "checkbox", + name = "research_sync_follow", + caption = "Follow the cluster research queue", + tooltip = "On: this server's research queue is replaced by the cluster's, " + .. "and changes here change it on every following server.\n" + .. "Off: this server's research queue is cleared and it picks its own research.", + state = true, + } + end + frame["research_sync_follow"].state = get_script_data().follow ~= false +end + +if v2_tech_progress then + sync.events[defines.events.on_player_created] = function(event) + ensure_gui(game.get_player(event.player_index) --[[@as LuaPlayer]]) + end + sync.events[defines.events.on_player_joined_game] = sync.events[defines.events.on_player_created] + + sync.events[defines.events.on_gui_checked_state_changed] = function(event) + if event.element.name == "research_sync_follow" then + local player = game.get_player(event.player_index) --[[@as LuaPlayer]] + research_sync.set_follow(event.element.state, player.name) + end + end + + local function on_queue_event(event) + if event.force.name ~= "player" then + return + end + local script_data = get_script_data() + if script_data.follow ~= true or script_data.ignore_queue_events or script_data.ignore_research_finished then + return + end + script_data.queue_dirty = true + if event.player_index then + local player = game.get_player(event.player_index) --[[@as LuaPlayer]] + script_data.queue_player = player.name + end + end + sync.events[defines.events.on_research_queued] = on_queue_event + sync.events[defines.events.on_research_moved] = on_queue_event + sync.events[defines.events.on_research_cancelled] = on_queue_event + + -- One send per tick however many events fired (a lab raises several at once). + sync.events[defines.events.on_tick] = function() + local script_data = get_script_data() + if not script_data.queue_dirty then + return + end + script_data.queue_dirty = false + local player = script_data.queue_player + script_data.queue_player = nil + if script_data.follow ~= true then + return + end + clusterio_api.send_json("research_sync:queue", { + queue = queue_names(game.forces["player"]), + player = player, + }) + end +end + research_sync = {} function research_sync.dump_technologies() local force = game.forces["player"] @@ -269,5 +390,94 @@ function research_sync.research_technology(name, level) } end +function research_sync.queue_state() + if not v2_tech_progress then + rcon.print('{"follow":false}') + return + end + local script_data = get_script_data() + local first = script_data.follow == nil + if first then + script_data.follow = true + end + rcon.print(compat.table_to_json({ + follow = script_data.follow, + first = first, + queue = queue_names(game.forces["player"]), + })) +end + +--- @param data string JSON {queue: string[], player?: string, instance?: string} +function research_sync.apply_queue(data) + if not v2_tech_progress then + return + end + local msg = compat.json_to_table(data) --[[@as table]] + local queue = msg.queue or {} + if msg.instance then + local list = format_queue(queue) + if msg.player then + game.print(msg.player .. " changed the research queue on " .. msg.instance .. ": " .. list) + else + game.print("The research queue was changed on " .. msg.instance .. ": " .. list) + end + end + + if get_script_data().follow ~= true then + return + end + local force = game.forces["player"] + -- Names this tile has no technology for (a mod it lacks) would throw. + local known = {} + for _, name in ipairs(queue) do + if force.technologies[name] then + table.insert(known, name) + end + end + local current = queue_names(force) + local same = #known == #current + for i = 1, #known do + if known[i] ~= current[i] then + same = false + break + end + end + if not same then + write_queue(force, known) + end +end + +--- @param enabled boolean +--- @param player_name string? +function research_sync.set_follow(enabled, player_name) + if not v2_tech_progress then + return + end + local script_data = get_script_data() + if script_data.follow == enabled then + return + end + script_data.follow = enabled + script_data.queue_dirty = false + script_data.queue_player = nil + for _, player in pairs(game.players) do + ensure_gui(player) + end + + local state = enabled and "on" or "off" + if player_name then + game.print(player_name .. " turned " .. state .. " following the cluster research queue") + else + game.print("Following the cluster research queue was turned " .. state) + end + + local force = game.forces["player"] + if enabled then + clusterio_api.send_json("research_sync:follow", { queue = queue_names(force), player = player_name }) + else + write_queue(force, {}) + end +end + return sync