309 lines
12 KiB
Lua

Event = require('scripts/event')
min_attrition_rate = 0.0011
tickskip = 10
function is_system_force(force_name)
return force_name == "enemy"
or force_name == "neutral"
or force_name == "capture"
or force_name == "conquest"
or force_name == "ignore"
or force_name == "friendly"
end
function get_attrition_rate_for_surface(surface_index)
-- use cache
if global.suface_attrition_rates[surface_index] then
return global.suface_attrition_rates[surface_index]
end
-- or load
local rate = nil
local default_rate = settings.global["robot-attrition-factor"].value
for interface, functions in pairs(remote.interfaces) do
if functions["robot_attrition_for_surface"] then
local returned_rate = remote.call(interface, "robot_attrition_for_surface", {default_rate = default_rate, surface_index = surface_index})
if rate == nil or returned_rate > rate then
rate = returned_rate
end
end
end
if rate == nil then
rate = default_rate
end
global.suface_attrition_rates[surface_index] = rate
--game.print("Robot attrition rate for surface " .. surface_index .. " ("..game.surfaces[surface_index].name..") is " .. rate)
return rate
end
function get_crash_item(bot)
if global.crash_items[bot.name] then
if global.crash_items[bot.name] ~= "none" then
return global.crash_items[bot.name]
else
return nil
end
else
if bot.prototype.mineable_properties.products and bot.prototype.mineable_properties.products[1] then
local name = bot.prototype.mineable_properties.products[1].name.."-crashed"
if game.item_prototypes[name] then
global.crash_items[bot.name] = name
return name
end
end
end
global.crash_items[bot.name] = "none"
end
function get_bot_speed(name)
if not global.bot_speed then global.bot_speed = {} end
if not global.bot_speed[name] then
global.bot_speed[name] = game.entity_prototypes[name].speed
if global.bot_speed[name] >= 10000 then -- Cheat bot e.g. Editor Extensions
global.bot_speed[name] = 1
end
end
return global.bot_speed[name]
end
function get_bot_slow_speed_multiplier(name)
if not global.bot_slow_speed_multiplier then global.bot_slow_speed_multiplier = {} end
if not global.bot_slow_speed_multiplier[name] then
global.bot_slow_speed_multiplier[name] = game.entity_prototypes[name].speed_multiplier_when_out_of_energy
end
return global.bot_slow_speed_multiplier[name]
end
function bot_crash(bot, n_bots)
local inventory = bot.get_inventory(defines.inventory.robot_cargo)
-- defines.inventory.robot_cargo can only ever have 1 slot
if not inventory.is_empty() then
local drop = bot.surface.create_entity{
name = "logistic-robot-dropped-cargo",
position = {x = bot.position.x, y = bot.position.y + 1},
force = bot.force
}
local item_stack = inventory[1]
drop.get_inventory(defines.inventory.chest)[1].transfer_stack(item_stack) -- Preserves item-with-entity-data
drop.order_deconstruction(bot.force)
end
bot.force.kill_count_statistics.on_flow(bot.name, -1) --Track bot's death.
if global.forcedata and global.forcedata[bot.force.name] and global.forcedata[bot.force.name]["robot-attrition-explosion-safety"]
and n_bots <= 500 * global.forcedata[bot.force.name]["robot-attrition-explosion-safety"] then
--game.print("Skip explosion, n_bots "..n_bots.."<= ".. 500 * global.forcedata[bot.force.name]["robot-attrition-explosion-safety"])
else
bot.surface.create_entity{name = "robot-explosion", position=bot.position}
end
if bot.valid then
bot.force = "neutral" -- change force so that it does not cause death alerts
bot.die()
end
global.bots_crashed = (global.bots_crashed or 0) + 1 -- used as an achievement metric
end
function process_bot(bot, n_bots)
local force_speed_multiplier = 1 + bot.force.worker_robots_speed_modifier
local speed = get_bot_speed(bot.name) * force_speed_multiplier
local held_item_count = 0
if bot.energy > 0 then
local inventory = bot.get_inventory(defines.inventory.robot_cargo)
held_item_count = inventory.get_item_count()
else
speed = 0.5 * speed * get_bot_slow_speed_multiplier(bot.name)
end
local speed_items = speed * (held_item_count + 0.5) -- carrying itself counts as 0.5 items
local crash_score = speed_items
bot_crash(bot, n_bots)
return crash_score
end
function on_tick(event)
--[[
slowest funtions is by far: network.logistic_robots[i]
so only do that once per explosion.
which means that if a robot is selected it must die.
but risk factors should still be speed * items carried
so add these factors to the probability of the next selection round
factors apply multiplier to next selection phase
]]--
if not global.force_surfaces then return end
if game.tick % tickskip ~= 0 then return end
--game.forces[force].logistic_networks[network].logistic_robots :: array of LuaEntity
--for _, force in pairs(game.forces) do
for force_name, force_surfaces in pairs(global.force_surfaces) do
local force = game.forces[force_name]
if not force then
global.force_surfaces[force_name] = nil
else
local i = randint
--for surface_name, networks in pairs(force.logistic_networks) do
local force_logistic_networks = force.logistic_networks -- array of surface_name, networks
for surface_name, _ in pairs(force_surfaces) do
local surface = game.surfaces[surface_name]
if not surface then
force_surfaces[surface_name] = nil
else
local networks = force_logistic_networks[surface_name]
if networks then
local surface_attrition_rate = get_attrition_rate_for_surface(game.surfaces[surface_name].index)
if surface_attrition_rate > min_attrition_rate then
for _, network in pairs(networks) do
local n_bots = network.all_logistic_robots - network.available_logistic_robots
if n_bots > 50 then -- ignore small networks
if not global.forces[force.name] then global.forces[force.name] = {} end
if not global.forces[force.name][surface_name] then global.forces[force.name][surface_name] = { crash = 0, crash_rate = 0.1 } end
local crash_rate = global.forces[force.name][surface_name].crash_rate * tickskip * surface_attrition_rate / 1000000
local crash = global.forces[force.name][surface_name].crash + crash_rate * n_bots
if crash >= 1 then
local logistic_robots = network.logistic_robots
local to_crash = math.min(math.ceil(#logistic_robots/2), math.random(math.floor(crash))) -- don't crash all
local i = math.random(#logistic_robots) -- choose a starting bot
local crashed = 0
while crashed < to_crash do
-- then step through bots
i = (i % #logistic_robots) + 1
if logistic_robots[i] and logistic_robots[i].valid then
global.forces[force.name][surface_name].crash_rate = global.forces[force.name][surface_name].crash_rate * 0.9 + 0.1 * process_bot(logistic_robots[i], n_bots)
end -- if invalid bots were found just skip them anyway
crashed = crashed + 1
end
crash = crash - crashed
end
global.forces[force.name][surface_name].crash = crash
end
end
end
end
end
end
end
end
end
function on_init(event)
global.forces = {}
global.bot_speed = {}
global.bot_slow_speed_multiplier = {}
global.suface_attrition_rates = {}
global.robot_repair_setting = settings.startup["robot-attrition-repair"].value
global.crash_items = {}
end
function on_configuration_changed(event)
global.forces = {}
global.bot_speed = {}
global.bot_slow_speed_multiplier = {}
global.suface_attrition_rates = {} -- clear
if not global.force_surfaces then
for _, force in pairs(game.forces) do
if not is_system_force(force.name) then
for surface_name, networks in pairs(force.logistic_networks) do
for _, network in pairs(networks) do
if network.all_logistic_robots > 0 then
add_surface(force, game.surfaces[surface_name])
end
end
end
end
end
end
global.robot_repair_setting = settings.startup["robot-attrition-repair"].value
global.crash_items = {}
end
function on_runtime_mod_setting_changed(event)
if event.setting == "robot-attrition-factor" then
global.suface_attrition_rates = {} -- clear
end
end
-- Surface Gathering
function add_surface(force, surface)
if not is_system_force(force.name) then
if #force.players > 0 then
global.force_surfaces = global.force_surfaces or {}
global.force_surfaces[force.name] = global.force_surfaces[force.name] or {}
if not global.force_surfaces[force.name][surface.name] then
global.force_surfaces[force.name][surface.name] = game.tick
end
end
end
end
function on_built_roboport(event)
if not(event.created_entity and event.created_entity.valid) then return end
add_surface(event.created_entity.force, event.created_entity.surface)
end
function on_cloned_roboport(event)
if not(event.destination and event.destination.valid) then return end
add_surface(event.destination.force, event.destination.surface)
end
function on_script_built_roboport(event)
if not(event.entity and event.entity.valid) then return end
add_surface(event.entity.force, event.entity.surface)
end
script.on_event(defines.events.on_built_entity, on_built_roboport, {{filter = "type", type = "roboport"}})
script.on_event(defines.events.on_robot_built_entity, on_built_roboport, {{filter = "type", type = "roboport"}})
script.on_event(defines.events.on_entity_cloned, on_cloned_roboport, {{filter = "type", type = "roboport"}})
script.on_event(defines.events.script_raised_built, on_script_built_roboport, {{filter = "type", type = "roboport"}})
script.on_event(defines.events.script_raised_revive, on_script_built_roboport, {{filter = "type", type = "roboport"}})
local bot_remnants_set = nil
local function populate_remnant_set()
bot_remnants_set = {}
if settings.startup["robot-attrition-repair"].value == "Repair75" then
for bot_name, bot_prototype in pairs(game.get_filtered_entity_prototypes({{filter="type", type="logistic-robot"}})) do
local remnant_name = bot_name .. "-remnants"
if game.entity_prototypes[remnant_name] and game.entity_prototypes[remnant_name].type == "simple-entity" then
bot_remnants_set[remnant_name] = true
end
end
end
log("bot_remnants_set: " .. serpent.line(bot_remnants_set))
end
local function is_bot_remnant(entity_name)
if not bot_remnants_set then populate_remnant_set() end
return bot_remnants_set[entity_name]
end
-- if a bot corpse appears on a force's surface then order deconstruction
function on_trigger_created_entity(event)
local entity = event.entity
if is_bot_remnant(entity.name) and global.force_surfaces then
local surface_name = entity.surface.name
for force_name, surfaces in pairs(global.force_surfaces) do
if surfaces[surface_name] and entity.valid then -- Need to check for validity again as other mods may react to on_marked_for_deconstruction instantly
entity.order_deconstruction(force_name)
end
end
end
end
Event.addListener(defines.events.on_trigger_created_entity, on_trigger_created_entity)
-- Swarm safety
function on_research_finished(event)
local force = event.research.force
if event.research.name == "robot-attrition-explosion-safety" then
global.forcedata = global.forcedata or {}
global.forcedata[force.name] = global.forcedata[force.name] or {}
global.forcedata[force.name]["robot-attrition-explosion-safety"] = event.research.level - 1
end
end
Event.addListener(defines.events.on_research_finished, on_research_finished)
-- standard events
Event.addListener(defines.events.on_tick, on_tick)
Event.addListener(defines.events.on_runtime_mod_setting_changed, on_runtime_mod_setting_changed)
Event.addListener("on_init", on_init, true)
Event.addListener("on_configuration_changed", on_configuration_changed, true)