789 lines
35 KiB
Lua
789 lines
35 KiB
Lua
local Event = require('__stdlib__/stdlib/event/event').set_protected_mode(true)
|
|
local Area = require('__stdlib__/stdlib/area/area')
|
|
local Position = require('__stdlib__/stdlib/area/position')
|
|
local table = require('__stdlib__/stdlib/utils/table')
|
|
local time = require('__stdlib__/stdlib/utils/defines/time')
|
|
local Queue = require('scripts/hash_queue')
|
|
local queue
|
|
local cfg
|
|
|
|
-- Local functions for commonly used math functions
|
|
local max, floor = math.max, math.floor
|
|
|
|
local config = require('config')
|
|
local armormods = require('scripts/armor-mods')
|
|
local bot_radius = config.BOT_RADIUS
|
|
local queue_speed = config.QUEUE_SPEED_BONUS
|
|
|
|
local function unique(tbl)
|
|
return table.keys(table.invert(tbl))
|
|
end
|
|
local inv_list = unique {
|
|
defines.inventory.character_trash, defines.inventory.character_main, defines.inventory.god_main, defines.inventory.chest,
|
|
defines.inventory.character_vehicle, defines.inventory.car_trunk, defines.inventory.cargo_wagon
|
|
}
|
|
|
|
local explosives = {
|
|
{ name = 'cliff-explosives', count = 1 }, { name = 'explosives', count = 10 }, { name = 'explosive-rocket', count = 4 },
|
|
{ name = 'explosive-cannon-shell', count = 4 }, { name = 'cluster-grenade', count = 2 }, { name = 'grenade', count = 14 },
|
|
{ name = 'land-mine', count = 5 }, { name = 'artillery-shell', count = 1 }
|
|
}
|
|
|
|
local function update_settings()
|
|
local setting = settings['global']
|
|
cfg = {
|
|
poll_rate = setting['nanobots-nano-poll-rate'].value,
|
|
queue_rate = setting['nanobots-nano-queue-rate'].value,
|
|
queue_cycle = setting['nanobots-nano-queue-per-cycle'].value,
|
|
build_tiles = setting['nanobots-nano-build-tiles'].value,
|
|
network_limits = setting['nanobots-network-limits'].value,
|
|
nanobots_auto = setting['nanobots-nanobots-auto'].value,
|
|
equipment_auto = setting['nanobots-equipment-auto'].value,
|
|
afk_time = setting['nanobots-afk-time'].value * time.second,
|
|
do_proxies = setting['nanobots-nano-fullfill-requests'].value
|
|
}
|
|
end
|
|
Event.register(defines.events.on_runtime_mod_setting_changed, update_settings)
|
|
update_settings()
|
|
|
|
-- table.find functions
|
|
local table_find = table.find
|
|
|
|
-- return the name of the item found for table.find if we found at least 1 item or cheat_mode is enabled.
|
|
-- Don't return items with inventory
|
|
local _find_item = function(item_prototype, _, player, at_least_one)
|
|
local item, count = item_prototype.name, item_prototype.count
|
|
count = at_least_one and 1 or count
|
|
local prototype = game.item_prototypes[item]
|
|
if prototype.type ~= 'item-with-inventory' then
|
|
if player.cheat_mode or player.get_item_count(item) >= count then
|
|
return true
|
|
else
|
|
local vehicle = player.vehicle
|
|
local train = vehicle and vehicle.train
|
|
return vehicle and ((vehicle.get_item_count(item) >= count) or (train and train.get_item_count(item) >= count))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Is the player connected, not afk, and have an attached character
|
|
-- @param player: the player object
|
|
-- @return bool: player is connected and ready
|
|
local function is_connected_player_ready(player)
|
|
return (cfg.afk_time <= 0 or player.afk_time < cfg.afk_time) and player.character
|
|
end
|
|
|
|
local function has_powered_equipment(character, eq_name)
|
|
local grid = character.grid
|
|
if grid and grid.get_contents()[eq_name] then
|
|
return table_find(grid.equipment, function(v)
|
|
return v.name == eq_name and v.energy > 0
|
|
end)
|
|
end
|
|
end
|
|
|
|
-- Is the player not in a logistic network or has a working nano-interface
|
|
-- @param player: the player character object
|
|
-- @return bool: true if has chip or not in network
|
|
local function nano_network_check(character, e)
|
|
if has_powered_equipment(character, 'equipment-bot-chip-nanointerface') then
|
|
return true
|
|
else
|
|
local c = character
|
|
local networks = e and e.surface.find_logistic_networks_by_construction_area(e.position, e.force) or
|
|
c.surface.find_logistic_networks_by_construction_area(c.position, c.force)
|
|
-- Con bots in network
|
|
local pnetwork = c.logistic_cell and c.logistic_cell and c.logistic_cell.mobile and c.logistic_cell.logistic_network
|
|
local has_pbots = c.logistic_cell and c.logistic_cell.construction_radius > 0 and c.logistic_cell.logistic_network and
|
|
c.logistic_cell.logistic_network.all_construction_robots > 0
|
|
local has_nbots = table.any(networks, function(network)
|
|
return network ~= pnetwork and network.all_construction_robots > 0
|
|
end)
|
|
return not (has_pbots or has_nbots)
|
|
end
|
|
end
|
|
|
|
local moveables = { train = true, car = true, spidertron = true }
|
|
-- Can nanobots repair this entity.
|
|
-- @param entity: the entity object
|
|
-- @return bool: is damaged and repairable by nanobots
|
|
local function nano_repairable_entity(entity)
|
|
if entity.has_flag('not-repairable') or entity.type:find('robot') then
|
|
return false
|
|
end
|
|
-- Can't repair tracks with trains on them.
|
|
if (entity.type == 'straight-rail' or entity.type == 'curved-rail') and entity.minable == false then
|
|
return false
|
|
end
|
|
if (entity.get_health_ratio() or 1) >= 1 then
|
|
return false
|
|
end
|
|
if moveables[entity.type] and entity.speed > 0 then
|
|
return false
|
|
end
|
|
return table_size(entity.prototype.collision_mask) > 0
|
|
end
|
|
|
|
-- Get the gun, ammo and ammo name for the named gun: will return nil
|
|
-- for all returns if there is no ammo for the gun.
|
|
-- @param player: the player object
|
|
-- @param gun_name: the name of the gun to get
|
|
-- @return the gun object or nil
|
|
-- @return the ammo object or nil
|
|
-- @return string: the name of the ammo or nil
|
|
--- @param player LuaPlayer
|
|
local function get_gun_ammo_name(player, gun_name)
|
|
local gun_inv = player.get_inventory(defines.inventory.character_guns)
|
|
local ammo_inv = player.get_inventory(defines.inventory.character_ammo)
|
|
|
|
local gun --- @type LuaItemStack
|
|
local ammo --- @type LuaItemStack
|
|
|
|
if not player.mod_settings['nanobots-active-emitter-mode'].value then
|
|
local index
|
|
gun, index = gun_inv.find_item_stack(gun_name)
|
|
ammo = gun and ammo_inv[index]
|
|
else
|
|
local index = player.character.selected_gun_index
|
|
gun, ammo = gun_inv[index], ammo_inv[index]
|
|
end
|
|
|
|
if gun and gun.valid_for_read and gun.name == gun_name and ammo.valid_for_read then
|
|
return gun, ammo, ammo.name
|
|
end
|
|
return nil, nil, nil
|
|
end
|
|
|
|
-- Attempt to insert an item_stack or array of item_stacks into the entity
|
|
-- Spill to the ground at the entity/player anything that doesn't get inserted
|
|
-- @param entity: the entity or player object
|
|
-- @param item_stacks: a SimpleItemStack or array of SimpleItemStacks to insert
|
|
-- @return bool : there was some items inserted or spilled
|
|
local function insert_or_spill_items(entity, item_stacks, is_return_cheat)
|
|
if is_return_cheat then
|
|
return
|
|
end
|
|
|
|
local new_stacks = {}
|
|
if item_stacks then
|
|
if item_stacks[1] and item_stacks[1].name then
|
|
new_stacks = item_stacks
|
|
elseif item_stacks and item_stacks.name then
|
|
new_stacks = { item_stacks }
|
|
end
|
|
for _, stack in pairs(new_stacks) do
|
|
local name, count, health = stack.name, stack.count, stack.health or 1
|
|
if game.item_prototypes[name] and not game.item_prototypes[name].has_flag('hidden') then
|
|
local inserted = entity.insert({ name = name, count = count, health = health })
|
|
if inserted ~= count then
|
|
entity.surface.spill_item_stack(entity.position, { name = name, count = count - inserted, health = health }, true)
|
|
end
|
|
end
|
|
end
|
|
return new_stacks[1] and new_stacks[1].name and true
|
|
end
|
|
end
|
|
|
|
-- Attempt to insert an arrary of items stacks into an entity
|
|
-- @param entity: the entity object
|
|
-- @param item_stacks: a SimpleItemStack or array of SimpleitemStacks to insert
|
|
-- @return table: an array of SimpleItemStacks not inserted
|
|
local function insert_into_entity(entity, item_stacks)
|
|
item_stacks = item_stacks or {}
|
|
if item_stacks and item_stacks.name then
|
|
item_stacks = { item_stacks }
|
|
end
|
|
local new_stacks = {}
|
|
for _, stack in pairs(item_stacks) do
|
|
local name, count, health = stack.name, stack.count, stack.health or 1
|
|
local inserted = entity.insert(stack)
|
|
if inserted ~= count then
|
|
new_stacks[#new_stacks + 1] = { name = name, count = count - inserted, health = health }
|
|
end
|
|
end
|
|
return new_stacks
|
|
end
|
|
|
|
-- Scan the ground under a ghost entities collision box for items and insert them into the player.
|
|
-- @param entity: the entity object to scan under
|
|
-- @return table: a table of SimpleItemStacks or nil if empty
|
|
local function get_all_items_on_ground(entity, existing_stacks)
|
|
local item_stacks = existing_stacks or {}
|
|
local surface, position, bouding_box = entity.surface, entity.position, entity.ghost_prototype.selection_box
|
|
local area = Area.offset(bouding_box, position)
|
|
for _, item_on_ground in pairs(surface.find_entities_filtered { name = 'item-on-ground', area = area }) do
|
|
item_stacks[#item_stacks + 1] = { name = item_on_ground.stack.name, count = item_on_ground.stack.count, health = item_on_ground.health or 1 }
|
|
item_on_ground.destroy()
|
|
end
|
|
local inserter_area = Area.expand(area, 3)
|
|
for _, inserter in pairs(surface.find_entities_filtered { area = inserter_area, type = 'inserter' }) do
|
|
local stack = inserter.held_stack
|
|
if stack.valid_for_read and Position.inside(inserter.held_stack_position, area) then
|
|
item_stacks[#item_stacks + 1] = { name = stack.name, count = stack.count, health = stack.health or 1 }
|
|
stack.clear()
|
|
end
|
|
end
|
|
return (item_stacks[1] and item_stacks) or {}
|
|
end
|
|
|
|
-- Get items with health data from the inventory
|
|
-- @param entity: the entity object to search
|
|
-- @param item: the item to look for
|
|
-- @return item_stack; SimpleItemStack
|
|
local function get_items_from_inv(entity, item_stack, cheat, at_least_one)
|
|
if cheat then
|
|
return { name = item_stack.name, count = item_stack.count, health = 1 }
|
|
else
|
|
local sources
|
|
if entity.vehicle and entity.vehicle.train then
|
|
sources = entity.vehicle.train.cargo_wagons
|
|
sources[#sources + 1] = entity
|
|
elseif entity.vehicle then
|
|
sources = { entity.vehicle, entity }
|
|
else
|
|
sources = { entity }
|
|
end
|
|
|
|
local new_item_stack = { name = item_stack.name, count = 0, health = 1 }
|
|
|
|
local count = item_stack.count
|
|
|
|
for _, source in pairs(sources) do
|
|
for _, inv in pairs(inv_list) do
|
|
local inventory = source.get_inventory(inv)
|
|
if inventory and inventory.valid and inventory.get_item_count(item_stack.name) > 0 then
|
|
local stack = inventory.find_item_stack(item_stack.name)
|
|
while stack do
|
|
local removed = math.min(stack.count, count)
|
|
new_item_stack.count = new_item_stack.count + removed
|
|
new_item_stack.health = new_item_stack.health * stack.health
|
|
stack.count = stack.count - removed
|
|
count = count - removed
|
|
|
|
if new_item_stack.count == item_stack.count then
|
|
return new_item_stack
|
|
end
|
|
stack = inventory.find_item_stack(item_stack.name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- If we havn't returned here check the hand!
|
|
if entity.is_player() then
|
|
local stack = entity.cursor_stack
|
|
if stack and stack.valid_for_read and stack.name == item_stack.name then
|
|
local removed = math.min(stack.count, count)
|
|
new_item_stack.count = new_item_stack.count + removed
|
|
new_item_stack.health = new_item_stack.health * stack.health
|
|
stack.count = stack.count - count
|
|
end
|
|
end
|
|
if new_item_stack.count == item_stack.count then
|
|
return new_item_stack
|
|
elseif new_item_stack.count > 0 and at_least_one then
|
|
return new_item_stack
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Manually drain ammo, if it is the last bit of ammo in the stack pull in more ammo from inventory if available
|
|
-- @param player: the player object
|
|
-- @param ammo: the ammo itemstack
|
|
-- @return bool: this was the last one to be drained
|
|
local function ammo_drain(player, ammo, amount)
|
|
if player.cheat_mode then
|
|
return true
|
|
end
|
|
|
|
amount = amount or 1
|
|
local name = ammo.name
|
|
ammo.drain_ammo(amount)
|
|
if not ammo.valid_for_read then
|
|
local new = player.get_main_inventory().find_item_stack(name)
|
|
if new then
|
|
ammo.set_stack(new)
|
|
new.clear()
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- Get the radius to use based on tehnology and player defined radius
|
|
-- @param player: the player entity to check
|
|
-- @param nano_ammo: the ammo to check
|
|
local function get_ammo_radius(player, nano_ammo)
|
|
local data = global.players[player.index]
|
|
local max_radius = bot_radius[player.force.get_ammo_damage_modifier(nano_ammo.prototype.get_ammo_type().category)] or 7
|
|
local custom_radius = data.ranges[nano_ammo.name] or max_radius
|
|
return custom_radius <= max_radius and custom_radius or max_radius
|
|
end
|
|
|
|
-- Attempt to satisfy module requests from player inventory
|
|
-- @param requests: the item request proxy to get requests from
|
|
-- @param entity: the entity to satisfy requests for
|
|
-- @param player: the entity to get modules from
|
|
local function satisfy_requests(requests, entity, player)
|
|
local pinv = player.get_main_inventory()
|
|
local new_requests = {}
|
|
for name, count in pairs(requests.item_requests) do
|
|
if count > 0 and entity.can_insert(name) then
|
|
local removed = player.cheat_mode and count or pinv.remove({ name = name, count = count })
|
|
local inserted = removed > 0 and entity.insert({ name = name, count = removed }) or 0
|
|
local balance = count - inserted
|
|
new_requests[name] = balance > 0 and balance or nil
|
|
else
|
|
new_requests[name] = count
|
|
end
|
|
end
|
|
requests.item_requests = new_requests
|
|
end
|
|
|
|
-- Create a projectile from source to target
|
|
-- @param name: the name of the projecticle
|
|
-- @param surface: the surface to create the projectile on
|
|
-- @param force: the force this projectile belongs too
|
|
-- @param source: position table to start at
|
|
-- @param target: position table to end at
|
|
local function create_projectile(name, surface, force, source, target, speed)
|
|
speed = speed or 1
|
|
force = force or 'player'
|
|
surface.create_entity { name = name, force = force, position = source, target = target, speed = speed }
|
|
end
|
|
|
|
--[[Nano Emitter Queue Handler --]]
|
|
-- Queued items are handled one at a time, --check validity of all stored objects at this point, They could have become
|
|
-- invalidated between the time they were entered into the queue and now.
|
|
|
|
function Queue.cliff_deconstruction(data)
|
|
local entity, player = data.entity, game.get_player(data.player_index)
|
|
if not (player and player.valid) then
|
|
return
|
|
end
|
|
|
|
if not (entity and entity.valid and entity.to_be_deconstructed(player.force)) then
|
|
return insert_or_spill_items(player, { data.item_stack })
|
|
end
|
|
|
|
create_projectile('nano-projectile-deconstructors', entity.surface, entity.force, player.position, entity.position)
|
|
local exp_name = data.item_stack.name == 'artillery-shell' and 'big-artillery-explosion' or 'big-explosion'
|
|
entity.surface.create_entity { name = exp_name, position = entity.position }
|
|
entity.destroy({ do_cliff_correction = true, raise_destroy = true })
|
|
end
|
|
|
|
-- Handles all of the deconstruction and scrapper related tasks.
|
|
function Queue.deconstruction(data)
|
|
local entity, player = data.entity, game.get_player(data.player_index)
|
|
if not (player and player.valid) then
|
|
return
|
|
end
|
|
|
|
if not (entity and entity.valid and entity.to_be_deconstructed(player.force)) then
|
|
return
|
|
end
|
|
|
|
local surface, force = data.surface or entity.surface, data.force or entity.force
|
|
local ppos, epos = player.position, entity.position
|
|
|
|
create_projectile('nano-projectile-deconstructors', surface, force, ppos, epos)
|
|
create_projectile('nano-projectile-return', surface, force, epos, ppos)
|
|
|
|
if entity.name == 'deconstructible-tile-proxy' then
|
|
local tile = surface.get_tile(epos)
|
|
if tile then
|
|
player.mine_tile(tile)
|
|
entity.destroy()
|
|
end
|
|
else
|
|
player.mine_entity(entity)
|
|
end
|
|
end
|
|
|
|
function Queue.build_entity_ghost(data)
|
|
local ghost, player, ghost_surf, ghost_pos = data.entity, game.get_player(data.player_index), data.surface, data.position
|
|
if not (player and player.valid) then
|
|
return
|
|
end
|
|
|
|
if not (ghost.valid and data.entity.ghost_name == data.entity_name) then
|
|
return insert_or_spill_items(player, { data.item_stack }, player.cheat_mode)
|
|
end
|
|
|
|
local item_stacks = get_all_items_on_ground(ghost)
|
|
if not player.surface.can_place_entity { name = ghost.ghost_name, position = ghost.position, direction = ghost.direction, force = ghost.force } then
|
|
return insert_or_spill_items(player, { data.item_stack }, player.cheat_mode)
|
|
end
|
|
|
|
local revived, entity, requests = ghost.revive { return_item_request_proxy = true, raise_revive = true }
|
|
|
|
if not revived then
|
|
return insert_or_spill_items(player, { data.item_stack }, player.cheat_mode)
|
|
end
|
|
|
|
if not entity then
|
|
if insert_or_spill_items(player, item_stacks, player.cheat_mode) then
|
|
create_projectile('nano-projectile-return', ghost_surf, player.force, ghost_pos, player.position)
|
|
end
|
|
return
|
|
end
|
|
|
|
create_projectile('nano-projectile-constructors', entity.surface, entity.force, player.position, entity.position)
|
|
entity.health = (entity.health > 0) and ((data.item_stack.health or 1) * entity.prototype.max_health)
|
|
if insert_or_spill_items(player, insert_into_entity(entity, item_stacks)) then
|
|
create_projectile('nano-projectile-return', ghost_surf, player.force, ghost_pos, player.position)
|
|
end
|
|
if requests then
|
|
satisfy_requests(requests, entity, player)
|
|
end
|
|
end
|
|
|
|
function Queue.build_tile_ghost(data)
|
|
local ghost, surface, position, player = data.entity, data.surface, data.position, game.get_player(data.player_index)
|
|
if not (player and player.valid) then
|
|
return
|
|
end
|
|
|
|
if not ghost.valid then
|
|
return insert_or_spill_items(player, { data.item_stack })
|
|
end
|
|
|
|
local tile, hidden_tile = surface.get_tile(position), surface.get_hidden_tile(position)
|
|
local force = ghost.force
|
|
local tile_was_mined = hidden_tile and tile.prototype.can_be_part_of_blueprint and player.mine_tile(tile)
|
|
local ghost_was_revived = ghost.valid and ghost.revive({ raise_revive = true }) -- Mining tiles invalidates ghosts
|
|
if not (tile_was_mined or ghost_was_revived) then
|
|
return insert_or_spill_items(player, { data.item_stack })
|
|
end
|
|
|
|
local item_ptype = data.item_stack and game.item_prototypes[data.item_stack.name]
|
|
local tile_ptype = item_ptype and item_ptype.place_as_tile_result.result
|
|
create_projectile('nano-projectile-constructors', surface, force, player.position, position)
|
|
Position.floored(position)
|
|
-- if the tile was mined, we need to manually place the tile.
|
|
-- checking if the ghost was revived is likely unnecessary but felt safer.
|
|
if tile_was_mined and not ghost_was_revived then
|
|
create_projectile('nano-projectile-return', surface, force, position, player.position)
|
|
surface.set_tiles({ { name = tile_ptype.name, position = position } }, true, true, false, true)
|
|
end
|
|
|
|
surface.play_sound { path = 'nano-sound-build-tiles', position = position }
|
|
end
|
|
|
|
function Queue.upgrade_direction(data)
|
|
local ghost, player, surface = data.entity, game.get_player(data.player_index), data.surface
|
|
if not (player and player.valid) then
|
|
return
|
|
end
|
|
|
|
if not ghost.valid and not ghost.to_be_upgraded() then
|
|
return
|
|
end
|
|
|
|
ghost.direction = data.direction
|
|
ghost.cancel_upgrade(player.force, player)
|
|
create_projectile('nano-projectile-constructors', ghost.surface, ghost.force, player.position, ghost.position)
|
|
surface.play_sound { path = 'utility/build_small', position = ghost.position }
|
|
end
|
|
|
|
function Queue.upgrade_ghost(data)
|
|
local ghost, player, surface, position = data.entity, game.get_player(data.player_index), data.surface, data.position
|
|
if not (player and player.valid) then
|
|
return
|
|
end
|
|
|
|
if not ghost.valid then
|
|
return insert_or_spill_items(player, { data.item_stack })
|
|
end
|
|
|
|
local entity = surface.create_entity {
|
|
name = data.entity_name or data.item_stack.name,
|
|
direction = ghost.direction,
|
|
force = ghost.force,
|
|
position = position,
|
|
fast_replace = true,
|
|
player = player,
|
|
type = ghost.type == 'underground-belt' and ghost.belt_to_ground_type or nil,
|
|
raise_built = true
|
|
}
|
|
if not entity then
|
|
return insert_or_spill_items(player, { data.item_stack })
|
|
end
|
|
|
|
create_projectile('nano-projectile-constructors', entity.surface, entity.force, player.position, entity.position)
|
|
surface.play_sound { path = 'utility/build_small', position = entity.position }
|
|
entity.health = (entity.health > 0) and ((data.item_stack.health or 1) * entity.prototype.max_health)
|
|
end
|
|
|
|
function Queue.item_requests(data)
|
|
local proxy, player = data.entity, game.get_player(data.player_index)
|
|
local target = proxy.valid and proxy.proxy_target
|
|
if not (player and player.valid) then
|
|
return
|
|
end
|
|
|
|
if not (proxy.valid and target and target.valid) then
|
|
return insert_or_spill_items(player, { data.item_stack })
|
|
end
|
|
|
|
if not target.can_insert(data.item_stack) then
|
|
return insert_or_spill_items(player, { data.item_stack })
|
|
end
|
|
|
|
create_projectile('nano-projectile-constructors', proxy.surface, proxy.force, player.position, proxy.position)
|
|
local item_stack = data.item_stack
|
|
local requests = proxy.item_requests
|
|
local inserted = target.insert(item_stack)
|
|
item_stack.count = item_stack.count - inserted
|
|
|
|
if item_stack.count > 0 then
|
|
insert_or_spill_items(player, { item_stack })
|
|
end
|
|
|
|
requests[item_stack.name] = requests[item_stack.name] - inserted
|
|
for k, count in pairs(requests) do
|
|
if count == 0 then
|
|
requests[k] = nil
|
|
end
|
|
end
|
|
|
|
if table_size(requests) > 0 then
|
|
proxy.item_requests = requests
|
|
else
|
|
proxy.destroy()
|
|
end
|
|
end
|
|
|
|
--[[ Nano Emmitter --]]
|
|
-- Extension of the tick handler, This functions decide what to do with their
|
|
-- assigned robots and insert them into the queue accordingly.
|
|
-- TODO: replace table_find entity-match with hashed lookup
|
|
-- Nano Constructors
|
|
-- Queue the ghosts in range for building, heal stuff needing healed
|
|
--- @param player LuaPlayer
|
|
local function queue_ghosts_in_range(player, pos, nano_ammo)
|
|
-- local queue = global.nano_queue
|
|
local pdata = global.players[player.index]
|
|
local force = player.force
|
|
local _next_nano_tick = (pdata._next_nano_tick and pdata._next_nano_tick < (game.tick + 2000) and pdata._next_nano_tick) or game.tick
|
|
local tick_spacing = max(1, cfg.queue_rate - (queue_speed[force.get_gun_speed_modifier('nano-ammo')] or queue_speed[4]))
|
|
local next_tick, queue_count = queue:next(_next_nano_tick, tick_spacing)
|
|
local radius = get_ammo_radius(player, nano_ammo)
|
|
local area = Position.expand_to_area(pos, radius)
|
|
|
|
for _, ghost in pairs(player.surface.find_entities(area)) do
|
|
local same_force = ghost.force == force
|
|
local deconstruct = ghost.to_be_deconstructed()
|
|
local upgrade = ghost.to_be_upgraded() and ghost.force == force
|
|
|
|
if (deconstruct or upgrade or same_force) then
|
|
if nano_ammo.valid and nano_ammo.valid_for_read then
|
|
if not cfg.network_limits or nano_network_check(player.character, ghost) then
|
|
if queue_count() < cfg.queue_cycle then
|
|
if not queue:get_hash(ghost) then
|
|
local data = {
|
|
player_index = player.index,
|
|
ammo = nano_ammo,
|
|
position = ghost.position,
|
|
surface = ghost.surface,
|
|
unit_number = ghost.unit_number,
|
|
entity = ghost
|
|
}
|
|
if deconstruct then
|
|
if ghost.type == 'cliff' then
|
|
if player.force.technologies['nanobots-cliff'].researched then
|
|
local item_stack = table_find(explosives, _find_item, player)
|
|
if item_stack then
|
|
local explosive = get_items_from_inv(player, item_stack, player.cheat_mode)
|
|
if explosive then
|
|
data.item_stack = explosive
|
|
data.action = 'cliff_deconstruction'
|
|
queue:insert(data, next_tick())
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
end
|
|
end
|
|
elseif ghost.minable then
|
|
data.action = 'deconstruction'
|
|
data.deconstructors = true
|
|
queue:insert(data, next_tick())
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
elseif upgrade then
|
|
local prototype = ghost.get_upgrade_target()
|
|
if prototype then
|
|
if prototype.name == ghost.name then
|
|
local dir = ghost.get_upgrade_direction()
|
|
if ghost.direction ~= dir then
|
|
data.action = 'upgrade_direction'
|
|
data.direction = dir
|
|
queue:insert(data, next_tick())
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
else
|
|
local item_stack = table_find(prototype.items_to_place_this, _find_item, player)
|
|
if item_stack then
|
|
data.action = 'upgrade_ghost'
|
|
local place_item = get_items_from_inv(player, item_stack, player.cheat_mode)
|
|
if place_item then
|
|
data.entity_name = prototype.name
|
|
data.item_stack = place_item
|
|
queue:insert(data, next_tick())
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif ghost.name == 'entity-ghost' or (ghost.name == 'tile-ghost' and cfg.build_tiles) then
|
|
-- get first available item that places entity from inventory that is not in our hand.
|
|
local proto = ghost.ghost_prototype
|
|
local item_stack = table_find(proto.items_to_place_this, _find_item, player)
|
|
if item_stack then
|
|
if ghost.name == 'entity-ghost' then
|
|
local place_item = get_items_from_inv(player, item_stack, player.cheat_mode)
|
|
if place_item then
|
|
data.action = 'build_entity_ghost'
|
|
data.entity_name = proto.name
|
|
data.item_stack = place_item
|
|
queue:insert(data, next_tick())
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
elseif ghost.name == 'tile-ghost' then
|
|
-- Don't queue tile ghosts if entity ghost is on top of it.
|
|
if ghost.surface.count_entities_filtered { name = 'entity-ghost', area = Area(ghost.bounding_box):non_zero(), limit = 1 } ==
|
|
0 then
|
|
local tile = ghost.surface.get_tile(ghost.position)
|
|
if tile then
|
|
local place_item = get_items_from_inv(player, item_stack, player.cheat_mode)
|
|
if place_item then
|
|
data.item_stack = place_item
|
|
data.action = 'build_tile_ghost'
|
|
queue:insert(data, next_tick())
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif nano_repairable_entity(ghost) then
|
|
-- Check if entity needs repair, TODO: Better logic for this?
|
|
if ghost.surface.count_entities_filtered { name = 'nano-cloud-small-repair', position = ghost.position } == 0 then
|
|
ghost.surface.create_entity {
|
|
name = 'nano-projectile-repair',
|
|
position = player.position,
|
|
force = force,
|
|
target = ghost.position,
|
|
speed = 0.5
|
|
}
|
|
queue_count(1)
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end -- repair
|
|
elseif ghost.name == 'item-request-proxy' and cfg.do_proxies then
|
|
local items = {}
|
|
for item, count in pairs(ghost.item_requests) do
|
|
items[#items + 1] = { name = item, count = count }
|
|
end
|
|
local item_stack = table_find(items, _find_item, player, true)
|
|
if item_stack then
|
|
local place_item = get_items_from_inv(player, item_stack, player.cheat_mode, true)
|
|
if place_item and place_item.count > 0 then
|
|
data.action = 'item_requests'
|
|
data.item_stack = place_item
|
|
queue:insert(data, next_tick())
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
end
|
|
end -- deconstruct, build or repair
|
|
end -- hash_check()
|
|
else
|
|
break
|
|
end -- queue_count()
|
|
end -- network check
|
|
else
|
|
-- ran out of ammo, break out here
|
|
break
|
|
end -- valid ammo
|
|
end -- not flag not_on_map
|
|
end -- looping through entities
|
|
pdata._next_nano_tick = next_tick() or game.tick
|
|
end -- function
|
|
|
|
-- Nano Termites
|
|
-- Kill the trees! Kill them dead
|
|
local function everyone_hates_trees(player, pos, nano_ammo)
|
|
local radius = get_ammo_radius(player, nano_ammo)
|
|
local force = player.force
|
|
for _, stupid_tree in pairs(player.surface.find_entities_filtered { position = pos, radius = radius, type = 'tree', limit = 200 }) do
|
|
if nano_ammo.valid and nano_ammo.valid_for_read then
|
|
if not stupid_tree.to_be_deconstructed(player.force) then
|
|
local tree_area = Area.expand(stupid_tree.bounding_box, .5)
|
|
if player.surface.count_entities_filtered { area = tree_area, name = 'nano-cloud-small-termites' } == 0 then
|
|
player.surface
|
|
.create_entity { name = 'nano-projectile-termites', position = player.position, force = force, target = stupid_tree, speed = .5 }
|
|
ammo_drain(player, nano_ammo, 1)
|
|
end
|
|
end
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
--[[ EVENTS --]]
|
|
-- The Tick Handler
|
|
local function poll_players(event)
|
|
-- Run logic for nanobots and power armor modules
|
|
-- if event.tick % math.ceil(#game.connected_players/cfg.poll_rate) == 0 then
|
|
if event.tick % max(1, floor(cfg.poll_rate / #game.connected_players)) == 0 then
|
|
local last_player, player = next(game.connected_players, global._last_player)
|
|
-- Establish connected, non afk, player character
|
|
if player and is_connected_player_ready(player) then
|
|
if cfg.nanobots_auto and (not cfg.network_limits or nano_network_check(player.character)) then
|
|
local gun, nano_ammo, ammo_name = get_gun_ammo_name(player, 'gun-nano-emitter')
|
|
if gun then
|
|
if ammo_name == 'ammo-nano-constructors' then
|
|
queue_ghosts_in_range(player, player.position, nano_ammo)
|
|
elseif ammo_name == 'ammo-nano-termites' then
|
|
everyone_hates_trees(player, player.position, nano_ammo)
|
|
end
|
|
end -- Gun and Ammo check
|
|
end
|
|
if cfg.equipment_auto then
|
|
armormods.prepare_chips(player)
|
|
end -- Auto Equipment
|
|
end -- Player Ready
|
|
global._last_player = last_player
|
|
end -- NANO Automatic scripts
|
|
queue:execute(event)
|
|
end
|
|
Event.register(defines.events.on_tick, poll_players)
|
|
|
|
-- Reset last player when players join or leave
|
|
local function players_changed()
|
|
global._last_player = nil
|
|
end
|
|
Event.register({ defines.events.on_player_joined_game, defines.events.on_player_left_game }, players_changed)
|
|
|
|
local function on_nano_init()
|
|
global.nano_queue = Queue()
|
|
queue = global.nano_queue
|
|
game.print('Nanobots are now ready to serve')
|
|
end
|
|
Event.register(Event.core_events.init, on_nano_init)
|
|
|
|
local function on_nano_load()
|
|
queue = Queue(global.nano_queue)
|
|
end
|
|
Event.register(Event.core_events.load, on_nano_load)
|
|
|
|
local function reset_nano_queue()
|
|
global.nano_queue = nil
|
|
queue = nil
|
|
global.nano_queue = Queue()
|
|
queue = global.nano_queue
|
|
for _, player in pairs(global.players) do
|
|
player._next_nano_tick = 0
|
|
end
|
|
end
|
|
Event.register(Event.generate_event_name('reset_nano_queue'), reset_nano_queue)
|