diff --git a/.vscode/launch.json b/.vscode/launch.json index 19be37f..216cfe6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -52,7 +52,6 @@ "adjustMods": { "debugadapter": true, "flib": true, - "cybersyn": true, }, } ] diff --git a/README.md b/README.md index cc13351..b7c7b18 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Stations can **automatically build allow-lists for trains** they can load or unl **Improved fault handling.** Mistakes and misconfigured stations are unlikely to result in items being delivered to places they shouldn't, and the player will be alerted immediately about the fault. -Runs a custom, **highly optimized central planning algorithm**, resulting in exceptionally good performance. Outperforms LTN in testing *(disclaimer: there is no perfectly apples-to-apples performance test since the features and algorithms of these mods are not the same)*. The station update rate is twice that of LTN by default, and depots don't count towards station updates. +Runs a custom, **highly optimized central planning algorithm**, resulting in exceptionally good performance. Greatly outperforms LTN in testing *(disclaimer: there is no perfectly apples-to-apples performance test since the features and algorithms of these mods are not the same, see /previews/performance/ )*. The station update rate is twice that of LTN by default, and depots don't count towards station updates. ![Alt text](/previews/se-compat.png) diff --git a/cybersyn/changelog.txt b/cybersyn/changelog.txt index fb4d224..7f267bf 100644 --- a/cybersyn/changelog.txt +++ b/cybersyn/changelog.txt @@ -71,3 +71,8 @@ Date: 2022-11-29 Features: - Added mod description - Added update rate setting +--------------------------------------------------------------------------------------------------- +Version: 1.0.3 +Date: 2022-11-30 + Features: + - Fixed a bug where duplicate orders could not be prevented on stations that share the same pool of items diff --git a/cybersyn/info.json b/cybersyn/info.json index 3239907..1a86e66 100644 --- a/cybersyn/info.json +++ b/cybersyn/info.json @@ -1,10 +1,10 @@ { "name": "cybersyn", - "version": "1.0.2", + "version": "1.0.3", "title": "Project Cybersyn", "author": "Mami", "factorio_version": "1.1", - "description": "Adds cybernetic combinators to the game, which connect to adjacent train stops to create a train logistics network. With just this mod you can coordinate the economic inputs and outputs of your entire megabase.", + "description": "Creates a feature-rich train logistics network through cybernetic combinators. With just this mod you can coordinate the economic inputs and outputs of your entire megabase.", "dependencies": [ "base", "flib >= 0.6.0", diff --git a/cybersyn/scripts/central-planning.lua b/cybersyn/scripts/central-planning.lua index a8332cd..d14e206 100644 --- a/cybersyn/scripts/central-planning.lua +++ b/cybersyn/scripts/central-planning.lua @@ -118,6 +118,7 @@ local function send_train_between(map_data, r_station_id, p_station_id, depot, p if r_effective_item_count < 0 and r_item_count < 0 then local r_threshold = r_station.p_count_or_r_threshold_per_item[item_name] local p_effective_item_count = p_station.p_count_or_r_threshold_per_item[item_name] + --could be an item that is not present at the station if p_effective_item_count and p_effective_item_count >= r_threshold then local item = {name = item_name, type = item_type, count = min(-r_effective_item_count, p_effective_item_count)} if item_name == primary_item_name then @@ -142,6 +143,9 @@ local function send_train_between(map_data, r_station_id, p_station_id, depot, p local i = 1 while i <= #manifest do local item = manifest[i] + if item.count < 1000 then + local hello = true + end local keep_item = false if item.type == "fluid" then if total_liquid_left > 0 then @@ -181,25 +185,10 @@ local function send_train_between(map_data, r_station_id, p_station_id, depot, p p_station.deliveries[item.name] = (p_station.deliveries[item.name] or 0) - item.count if item_i > 1 then + --prevent deliveries from being processed for these items until their stations are re-polled local item_network_name = network_name..":"..item.name - local r_stations = economy.all_r_stations[item_network_name] - local p_stations = economy.all_p_stations[item_network_name] - if r_stations then - for j, id in ipairs(r_stations) do - if id == r_station_id then - table_remove(r_stations, j) - break - end - end - end - if p_stations then - for j, id in ipairs(p_stations) do - if id == p_station_id then - table_remove(p_stations, j) - break - end - end - end + economy.all_r_stations[item_network_name] = nil + economy.all_p_stations[item_network_name] = nil end end @@ -358,59 +347,40 @@ local function tick_dispatch(map_data, mod_settings) --psuedo-randomize what item (and what station) to check first so if trains available is low they choose orders psuedo-randomly --NOTE: It may be better for performance to update stations one tick at a time rather than all at once, however this does mean more redundant data will be generated and discarded each tick. Once we have a performance test-bed it will probably be worth checking. --NOTE: this is an approximation algorithm for solving the assignment problem (bipartite graph weighted matching), the true solution would be to implement the simplex algorithm but I strongly believe most factorio players would prefer run-time efficiency over perfect train routing logic - local tick_data = map_data.tick_data local all_r_stations = map_data.economy.all_r_stations local all_p_stations = map_data.economy.all_p_stations local all_names = map_data.economy.all_names local stations = map_data.stations - ---@type {} - local r_stations = tick_data.r_stations - ---@type {} - local p_stations = tick_data.p_stations - if p_stations == nil or #p_stations == 0 or #r_stations == 0 then - while true do - local size = #all_names - if size == 0 then - tick_data.r_stations = nil - tick_data.p_stations = nil - tick_data.item_name = nil - tick_data.item_type = nil - map_data.tick_state = STATE_INIT - return true - end + local r_stations + local p_stations + local item_name + local item_type + while true do + local size = #all_names + if size == 0 then + map_data.tick_state = STATE_INIT + return true + end - --randomizing the ordering should only matter if we run out of available trains - local name_i = size <= 2 and 2 or 2*random(size/2) - local item_network_name = all_names[name_i - 1] - local signal = all_names[name_i] + --randomizing the ordering should only matter if we run out of available trains + local name_i = size <= 2 and 2 or 2*random(size/2) - --swap remove - all_names[name_i - 1] = all_names[size - 1] - all_names[name_i] = all_names[size] - all_names[size] = nil - all_names[size - 1] = nil + local item_network_name = all_names[name_i - 1]--[[@as string]] + local signal = all_names[name_i]--[[@as SignalID]] - r_stations = all_r_stations[item_network_name] - p_stations = all_p_stations[item_network_name] + --swap remove + all_names[name_i - 1] = all_names[size - 1] + all_names[name_i] = all_names[size] + all_names[size] = nil + all_names[size - 1] = nil + + r_stations = all_r_stations[item_network_name] + p_stations = all_p_stations[item_network_name] + if r_stations then if p_stations then - tick_data.r_stations = r_stations - tick_data.p_stations = p_stations - tick_data.item_name = signal.name--[[@as string]] - tick_data.item_type = signal.type - table_sort(r_stations, function(a_id, b_id) - local a = stations[a_id] - local b = stations[b_id] - if a and b then - if a.priority ~= b.priority then - return a.priority < b.priority - else - return a.last_delivery_tick > b.last_delivery_tick - end - else - return a == nil - end - end) + item_name = signal.name--[[@as string]] + item_type = signal.type break else for i, id in ipairs(r_stations) do @@ -423,57 +393,80 @@ local function tick_dispatch(map_data, mod_settings) end end end + local max_threshold = INF + while true do + local r_station_id = nil + local r_threshold = nil + local best_prior = -INF + local best_lru = INF + for i, id in ipairs(r_stations) do + local station = stations[id] + --NOTE: the station at r_station_id could have been deleted and reregistered since last poll, this check here prevents it from being processed for a delivery in that case + if station and station.deliveries_total < station.entity_stop.trains_limit then + local threshold = station.p_count_or_r_threshold_per_item[item_name] + if threshold <= max_threshold and (station.priority > best_prior or (station.priority == best_prior and station.last_delivery_tick < best_lru)) then + r_station_id = id + r_threshold = threshold + best_prior = station.priority + best_lru = station.last_delivery_tick + end + end + end + if not r_station_id then + return false + end - local r_station_id = table_remove(r_stations--[[@as uint[] ]]) - local r_station = stations[r_station_id] - if r_station and r_station.deliveries_total < r_station.entity_stop.trains_limit then - local item_name = tick_data.item_name - local item_type = tick_data.item_type - --NOTE: the station at r_station_id could have been deleted and reregistered since last poll, this check here prevents it from being processed for a delivery in that case - local r_threshold = r_station.p_count_or_r_threshold_per_item[item_name] + local r_station = stations[r_station_id] - if r_threshold then - local best = 0 - local best_depot = nil - local best_dist = INF - local highest_prior = -INF - local can_be_serviced = false - for j, p_station_id in ipairs(p_stations) do - local p_station = stations[p_station_id] - if p_station and (p_station.p_count_or_r_threshold_per_item[item_name] or -1) >= r_threshold and p_station.deliveries_total < p_station.entity_stop.trains_limit then + local pre_max_threshold = max_threshold + max_threshold = 0 + local best_i = 0 + local best_depot = nil + local best_dist = INF + local best_prior = -INF + local can_be_serviced = false + for j, p_station_id in ipairs(p_stations) do + local p_station = stations[p_station_id] + if p_station and p_station.deliveries_total < p_station.entity_stop.trains_limit then + local effective_count = p_station.p_count_or_r_threshold_per_item[item_name] + if effective_count >= r_threshold then local prior = p_station.priority local slot_threshold = item_type == "fluid" and r_threshold or ceil(r_threshold/get_stack_size(map_data, item_name)) local depot, d = get_valid_train(map_data, r_station_id, p_station_id, item_type, slot_threshold) - if prior > highest_prior or (prior == highest_prior and d < best_dist) then + if prior > best_prior or (prior == best_prior and d < best_dist) then if depot then - best = j + best_i = j best_dist = d best_depot = depot - highest_prior = prior + best_prior = prior can_be_serviced = true elseif d < INF then - best = j + best_i = j can_be_serviced = true end end + elseif effective_count < pre_max_threshold and effective_count > max_threshold then + --set the max_threshold to the highest seen number that is strictly lower that the previous used + --due to where in the algorithm we are this will find a valid request and provide pair or abort in just one iteration + max_threshold = effective_count end end - if - best_depot and ( - best_depot.entity_comb.status == defines.entity_status.working or - best_depot.entity_comb.status == defines.entity_status.low_power) - then - send_train_between(map_data, r_station_id, table_remove(p_stations--[[@as {}]], best), best_depot, item_name) - else - if can_be_serviced then - send_missing_train_alert_for_stops(r_station.entity_stop, stations[p_stations--[[@as {}]][best]].entity_stop) - end - r_station.display_failed_request = true - r_station.display_update = true - end + end + if + best_depot and ( + best_depot.entity_comb.status == defines.entity_status.working or + best_depot.entity_comb.status == defines.entity_status.low_power) + then + send_train_between(map_data, r_station_id, table_remove(p_stations, best_i), best_depot, item_name) + return false + else + if can_be_serviced then + send_missing_train_alert_for_stops(r_station.entity_stop, stations[p_stations[best_i]].entity_stop) + end + r_station.display_failed_request = true + r_station.display_update = true end end - return false end ---@param map_data MapData ---@param mod_settings CybersynModSettings @@ -504,6 +497,8 @@ function tick(map_data, mod_settings) if tick_poll_station(map_data, mod_settings) then break end end elseif map_data.tick_state == STATE_DISPATCH then - tick_dispatch(map_data, mod_settings) + for i = 1, mod_settings.update_rate do + tick_dispatch(map_data, mod_settings) + end end end diff --git a/cybersyn/scripts/factorio-api.lua b/cybersyn/scripts/factorio-api.lua index 6f64eb4..d910a71 100644 --- a/cybersyn/scripts/factorio-api.lua +++ b/cybersyn/scripts/factorio-api.lua @@ -342,11 +342,16 @@ function set_combinator_output(map_data, comb, signals) end end +local WORKING = defines.entity_status.working +local LOW_POWER = defines.entity_status.low_power ---@param station Station function get_signals(station) local comb = station.entity_comb1 - if comb.valid and (comb.status == defines.entity_status.working or comb.status == defines.entity_status.low_power) then - return comb.get_merged_signals(defines.circuit_connector_id.combinator_input) + if comb.valid then + local status = comb.status + if status == WORKING or status == LOW_POWER then + return comb.get_merged_signals(defines.circuit_connector_id.combinator_input) + end else return nil end diff --git a/cybersyn/scripts/migrations.lua b/cybersyn/scripts/migrations.lua index 163378b..2606f85 100644 --- a/cybersyn/scripts/migrations.lua +++ b/cybersyn/scripts/migrations.lua @@ -117,7 +117,13 @@ local migrations_table = { for id, station in pairs(map_data.stations) do reset_station_layout(map_data, station) end - end + end, + ["1.0.3"] = function() + ---@type MapData + local map_data = global + map_data.tick_state = STATE_INIT + map_data.tick_data = {} + end, } ---@param data ConfigurationChangedData diff --git a/description.md b/description.md index 4b98820..8459c16 100644 --- a/description.md +++ b/description.md @@ -1,6 +1,6 @@ # Project Cybersyn -Behold one of the most feature-rich and performant logistics mods Factorio has to offer. Named for [Project Cybersyn](https://en.wikipedia.org//wiki/Project_Cybersyn) of Allende's Chile, with just this mod you can coordinate the economic inputs and outputs of your entire megabase. +Behold one of the most feature-rich and performant logistics mods Factorio has to offer. Named for [Project Cybersyn](https://en.wikipedia.org//wiki/Project_Cybersyn) of Allende's Chile, with just this mod you can coordinate the economic inputs and outputs of your entire megabase. Similar in functionality to the famous Logistics Train Network mod, but with a much broader scope. ![Image](https://github.com/mamoniot/project-cybersyn/blob/main/previews/outpost-resupply-station.png?raw=true) @@ -8,7 +8,7 @@ Behold one of the most feature-rich and performant logistics mods Factorio has t ![Image](https://github.com/mamoniot/project-cybersyn/blob/main/previews/gui-modes.png?raw=true) -**Intuitive and easy to learn**, without sacrificing features. Configure your stations using just 3 virtual signals, a couple of combinator settings and the train stop's own train limit. +**Intuitive and easy to learn**, without sacrificing features. Eases the player into building a train logistics network using parallels between the robot logistics network. Configure your stations using just 3 virtual signals, a couple of combinator settings and the train stop's own train limit. ### A whole suite of new and optional circuit network inputs and outputs to control your stations precisely * Natively read out all deliveries currently in progress for a station, not just the loading or unloading orders of the parked train. diff --git a/previews/performance/cybersyn-400smp-mods.png b/previews/performance/cybersyn-400smp-mods.png new file mode 100644 index 0000000..38de7ae Binary files /dev/null and b/previews/performance/cybersyn-400smp-mods.png differ diff --git a/previews/performance/cybersyn-400smp-performance.png b/previews/performance/cybersyn-400smp-performance.png new file mode 100644 index 0000000..f7f8d52 Binary files /dev/null and b/previews/performance/cybersyn-400smp-performance.png differ diff --git a/previews/performance/cybersyn-400smp-production.png b/previews/performance/cybersyn-400smp-production.png new file mode 100644 index 0000000..03f31d7 Binary files /dev/null and b/previews/performance/cybersyn-400smp-production.png differ diff --git a/previews/performance/cybersyn-400smp-settings.png b/previews/performance/cybersyn-400smp-settings.png new file mode 100644 index 0000000..85c2562 Binary files /dev/null and b/previews/performance/cybersyn-400smp-settings.png differ diff --git a/previews/performance/cybersyn-400smp-world.png b/previews/performance/cybersyn-400smp-world.png new file mode 100644 index 0000000..b4c3534 Binary files /dev/null and b/previews/performance/cybersyn-400smp-world.png differ diff --git a/previews/performance/ltn-60smp-mods.png b/previews/performance/ltn-60smp-mods.png new file mode 100644 index 0000000..d3aa690 Binary files /dev/null and b/previews/performance/ltn-60smp-mods.png differ diff --git a/previews/performance/ltn-60smp-performance.png b/previews/performance/ltn-60smp-performance.png new file mode 100644 index 0000000..19d71b4 Binary files /dev/null and b/previews/performance/ltn-60smp-performance.png differ diff --git a/previews/performance/ltn-60smp-production.png b/previews/performance/ltn-60smp-production.png new file mode 100644 index 0000000..9f36dd6 Binary files /dev/null and b/previews/performance/ltn-60smp-production.png differ diff --git a/previews/performance/ltn-60smp-settings.png b/previews/performance/ltn-60smp-settings.png new file mode 100644 index 0000000..11dc285 Binary files /dev/null and b/previews/performance/ltn-60smp-settings.png differ diff --git a/previews/performance/ltn-60smp-world.png b/previews/performance/ltn-60smp-world.png new file mode 100644 index 0000000..48c6024 Binary files /dev/null and b/previews/performance/ltn-60smp-world.png differ