mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23:04:52 +09:00
The last event of a tick now decides which player the queue edit is attributed to. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
485 lines
13 KiB
Lua
485 lines
13 KiB
Lua
local clusterio_api = require("modules/clusterio/api")
|
|
local compat = require("modules/clusterio/compat")
|
|
|
|
local v2_tech_progress = compat.version_ge("2.0.0")
|
|
-- on_research_queued and player_index on moved/cancelled arrived in 2.0.56
|
|
local queue_sync = compat.version_ge("2.0.56")
|
|
|
|
local sync = {}
|
|
|
|
--- @param tech LuaTechnology
|
|
local function get_technology_progress(tech)
|
|
if tech == tech.force.current_research then
|
|
return tech.force.research_progress
|
|
elseif v2_tech_progress then
|
|
return tech.saved_progress
|
|
else
|
|
--- @diagnostic disable-next-line
|
|
return tech.force.get_saved_technology_progress(tech.name)
|
|
end
|
|
end
|
|
|
|
--- @param tech LuaTechnology
|
|
--- @param progress number
|
|
local function set_technology_progress(tech, progress)
|
|
if tech == tech.force.current_research then
|
|
tech.force.research_progress = progress
|
|
elseif v2_tech_progress then
|
|
tech.saved_progress = progress
|
|
else
|
|
--- @diagnostic disable-next-line
|
|
tech.force.set_saved_technology_progress(tech.name, progress)
|
|
end
|
|
end
|
|
|
|
--- @class ResearchSyncTechnology
|
|
--- @field level uint
|
|
--- @field researched boolean?
|
|
--- @field progress double?
|
|
|
|
--- @class ResearchSyncData
|
|
--- @field technologies table<string, ResearchSyncTechnology>
|
|
--- @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
|
|
local function get_script_data(no_early_return)
|
|
local research_sync = compat.script_data.research_sync
|
|
if research_sync and not no_early_return then
|
|
return research_sync
|
|
end
|
|
|
|
research_sync = research_sync or {}
|
|
research_sync.technologies = research_sync.technologies or {}
|
|
|
|
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
|
|
local progress = get_technology_progress(tech)
|
|
research_sync.technologies[tech.name] = {
|
|
level = tech.level,
|
|
researched = tech.researched,
|
|
progress = progress,
|
|
}
|
|
end
|
|
|
|
compat.script_data.research_sync = research_sync
|
|
return research_sync
|
|
end
|
|
|
|
sync.events = {}
|
|
sync.events[clusterio_api.events.on_server_startup] = function(event)
|
|
get_script_data(true)
|
|
end
|
|
|
|
--- @param tech LuaTechnology
|
|
local function get_contribution(tech)
|
|
local progress = get_technology_progress(tech)
|
|
if not progress then
|
|
return 0, nil
|
|
end
|
|
|
|
local script_data = get_script_data()
|
|
local prev_tech = script_data.technologies[tech.name]
|
|
if prev_tech.progress and prev_tech.level == tech.level then
|
|
return progress - prev_tech.progress, progress
|
|
else
|
|
return progress, progress
|
|
end
|
|
end
|
|
|
|
--- @param tech LuaTechnology
|
|
local function send_contribution(tech)
|
|
local contribution, progress = get_contribution(tech)
|
|
if contribution ~= 0 then
|
|
clusterio_api.send_json("research_sync:contribution", {
|
|
name = tech.name,
|
|
level = tech.level,
|
|
contribution = contribution,
|
|
})
|
|
local script_data = get_script_data()
|
|
script_data.technologies[tech.name].progress = progress
|
|
end
|
|
end
|
|
|
|
--- @param event EventData.on_research_started
|
|
sync.events[defines.events.on_research_started] = function(event)
|
|
local tech = event.last_research
|
|
if tech then
|
|
send_contribution(tech)
|
|
end
|
|
end
|
|
|
|
--- @param event EventData.on_research_finished
|
|
sync.events[defines.events.on_research_finished] = function(event)
|
|
local script_data = get_script_data()
|
|
if script_data.ignore_research_finished then
|
|
return
|
|
end
|
|
|
|
local tech = event.research
|
|
script_data.technologies[tech.name] = {
|
|
level = tech.level,
|
|
researched = tech.researched,
|
|
}
|
|
|
|
local level = tech.level
|
|
if not tech.researched then
|
|
level = level - 1
|
|
end
|
|
|
|
clusterio_api.send_json("research_sync:finished", {
|
|
name = tech.name,
|
|
level = level,
|
|
})
|
|
end
|
|
|
|
sync.on_nth_tick = {}
|
|
sync.on_nth_tick[79] = function(event)
|
|
local tech = game.forces["player"].current_research
|
|
if tech then
|
|
send_contribution(tech)
|
|
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 queue_sync 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
|
|
-- the last event of the tick decides who gets named
|
|
local player = event.player_index and game.get_player(event.player_index)
|
|
script_data.queue_player = player and player.name or nil
|
|
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"]
|
|
|
|
local techs = {}
|
|
for _, tech in pairs(force.technologies) do
|
|
table.insert(techs, {
|
|
name = tech.name,
|
|
level = tech.level,
|
|
progress = get_technology_progress(tech),
|
|
researched = tech.researched,
|
|
})
|
|
end
|
|
|
|
if #techs == 0 then
|
|
rcon.print("[]")
|
|
else
|
|
rcon.print(compat.table_to_json(techs))
|
|
end
|
|
end
|
|
|
|
--- @param data string
|
|
function research_sync.sync_technologies(data)
|
|
local force = game.forces["player"]
|
|
|
|
local nameIndex = 1
|
|
local levelIndex = 2
|
|
local progressIndex = 3
|
|
local researchedIndex = 4
|
|
|
|
local script_data = get_script_data()
|
|
script_data.ignore_research_finished = true
|
|
for _, tech_data in pairs(compat.json_to_table(data) --[[@as table]]) do
|
|
local tech = force.technologies[tech_data[nameIndex]]
|
|
if tech and tech.level <= tech_data[levelIndex] then
|
|
local new_level = math.min(tech_data[levelIndex], tech.prototype.max_level)
|
|
if new_level ~= tech.level then
|
|
-- when the level of the current research changes the
|
|
-- progress is not automatically reset.
|
|
if force.current_research == tech then
|
|
force.research_progress = 0
|
|
end
|
|
tech.level = new_level
|
|
end
|
|
|
|
local progress
|
|
if tech_data[researchedIndex] then
|
|
if force.current_research == tech then
|
|
force.research_progress = 0
|
|
end
|
|
tech.researched = true
|
|
progress = nil
|
|
elseif tech_data[progressIndex] then
|
|
send_contribution(tech)
|
|
progress = tech_data[progressIndex]
|
|
set_technology_progress(tech, progress)
|
|
else
|
|
progress = get_technology_progress(tech)
|
|
end
|
|
|
|
script_data.technologies[tech.name] = {
|
|
level = tech.level,
|
|
researched = tech.researched,
|
|
progress = progress,
|
|
}
|
|
end
|
|
end
|
|
script_data.ignore_research_finished = false
|
|
end
|
|
|
|
--- @param data string
|
|
function research_sync.update_progress(data)
|
|
local script_data = get_script_data()
|
|
local techs = compat.json_to_table(data) --[[@as table]]
|
|
local force = game.forces["player"]
|
|
|
|
for _, controllerTech in ipairs(techs) do
|
|
local tech = force.technologies[controllerTech.name]
|
|
if tech and tech.level == controllerTech.level then
|
|
send_contribution(tech)
|
|
set_technology_progress(tech, controllerTech.progress)
|
|
script_data.technologies[tech.name] = {
|
|
level = tech.level,
|
|
progress = controllerTech.progress
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param name string
|
|
--- @param level number
|
|
function research_sync.research_technology(name, level)
|
|
local force = game.forces["player"]
|
|
local tech = force.technologies[name]
|
|
if not tech or tech.level > level then
|
|
return
|
|
end
|
|
|
|
if level > tech.prototype.max_level then
|
|
level = tech.prototype.max_level
|
|
end
|
|
|
|
local script_data = get_script_data()
|
|
script_data.ignore_research_finished = true
|
|
if tech == force.current_research and tech.level == level then
|
|
force.research_progress = 1
|
|
|
|
elseif tech.level < level or tech.level == level and not tech.researched then
|
|
tech.level = level
|
|
tech.researched = true
|
|
|
|
if tech.name:find("-%d+$") then
|
|
game.print {"", "Researched ", {"technology-name." .. tech.name:gsub("-%d+$", "")}, " ", level}
|
|
else
|
|
game.print {"", "Researched ", {"technology-name." .. tech.name}}
|
|
end
|
|
game.play_sound { path = "utility/research_completed" }
|
|
end
|
|
script_data.ignore_research_finished = false
|
|
|
|
script_data.technologies[tech.name] = {
|
|
level = tech.level,
|
|
researched = tech.researched,
|
|
}
|
|
end
|
|
|
|
function research_sync.queue_state()
|
|
if not queue_sync 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 queue_sync 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 queue_sync 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
|