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)