if mapProcessorG then return mapProcessorG end local mapProcessor = {} -- imports local pheromoneUtils = require("PheromoneUtils") local aiAttackWave = require("AIAttackWave") local aiPredicates = require("AIPredicates") local constants = require("Constants") local mapUtils = require("MapUtils") local playerUtils = require("PlayerUtils") local chunkUtils = require("ChunkUtils") local chunkPropertyUtils = require("ChunkPropertyUtils") local baseUtils = require("BaseUtils") local squadCompression = require("SquadCompression") -- constants local DURATION_ACTIVE_NEST_DIVIDER = constants.DURATION_ACTIVE_NEST_DIVIDER local DURATION_ACTIVE_NEST = constants.DURATION_ACTIVE_NEST local PROCESS_QUEUE_SIZE = constants.PROCESS_QUEUE_SIZE local RESOURCE_QUEUE_SIZE = constants.RESOURCE_QUEUE_SIZE local ENEMY_QUEUE_SIZE = constants.ENEMY_QUEUE_SIZE local PLAYER_QUEUE_SIZE = constants.PLAYER_QUEUE_SIZE local CLEANUP_QUEUE_SIZE = constants.CLEANUP_QUEUE_SIZE local CHUNK_SIZE = constants.CHUNK_SIZE local PROCESS_PLAYER_BOUND = constants.PROCESS_PLAYER_BOUND local CHUNK_TICK = constants.CHUNK_TICK local PROCESS_STATIC_QUEUE_SIZE = constants.PROCESS_STATIC_QUEUE_SIZE local AI_VENGENCE_SQUAD_COST = constants.AI_VENGENCE_SQUAD_COST local AI_STATE_AGGRESSIVE = constants.AI_STATE_AGGRESSIVE local AI_STATE_PEACEFUL = constants.AI_STATE_PEACEFUL local AI_STATE_MIGRATING = constants.AI_STATE_MIGRATING local AI_STATE_SIEGE = constants.AI_STATE_SIEGE local AI_STATE_GROWING = constants.AI_STATE_GROWING local GROWING_COOLDOWN = constants.GROWING_COOLDOWN local COOLDOWN_RALLY = constants.COOLDOWN_RALLY local COOLDOWN_RETREAT = constants.COOLDOWN_RETREAT local COOLDOWN_HUNT = constants.COOLDOWN_HUNT local BASE_PROCESS_INTERVAL = constants.BASE_PROCESS_INTERVAL -- imported functions local processStaticPheromone = pheromoneUtils.processStaticPheromone local processPheromone = pheromoneUtils.processPheromone local getDeathGenerator = chunkPropertyUtils.getDeathGenerator local processBase = baseUtils.processBase local processNestActiveness = chunkPropertyUtils.processNestActiveness local formSquads = aiAttackWave.formSquads local formVengenceSquad = aiAttackWave.formVengenceSquad local formSettlers = aiAttackWave.formSettlers local getChunkByPosition = mapUtils.getChunkByPosition local getChunkByXY = mapUtils.getChunkByXY local validPlayer = playerUtils.validPlayer local mapScanEnemyChunk = chunkUtils.mapScanEnemyChunk local mapScanPlayerChunk = chunkUtils.mapScanPlayerChunk local mapScanResourceChunk = chunkUtils.mapScanResourceChunk local registerEnemyBaseStructure = chunkUtils.registerEnemyBaseStructure local getNestCount = chunkPropertyUtils.getNestCount local getHiveCount = chunkPropertyUtils.getHiveCount local getTurretCount = chunkPropertyUtils.getTurretCount local getEnemyStructureCount = chunkPropertyUtils.getEnemyStructureCount local getNestActiveness = chunkPropertyUtils.getNestActiveness local getNestActiveTick = chunkPropertyUtils.getNestActiveTick local setNestActiveTick = chunkPropertyUtils.setNestActiveTick local getRaidNestActiveness = chunkPropertyUtils.getRaidNestActiveness local thisIsNewEnemyPosition = chunkPropertyUtils.thisIsNewEnemyPosition local getSquadsOnChunk = chunkPropertyUtils.getSquadsOnChunk local squadDecompress = squadCompression.squadDecompress local canAttack = aiPredicates.canAttack local canMigrate = aiPredicates.canMigrate local tableSize = table_size local mMin = math.min local mMax = math.max local next = next local mRandom = math.random -- module code --[[ processing is not consistant as it depends on the number of chunks that have been generated so if we process 400 chunks an iteration and 200 chunks have been generated than these are processed 3 times a second and 1200 generated chunks would be processed once a second In theory, this might be fine as smaller bases have less surface to attack and need to have pheromone dissipate at a faster rate. --]] function mapProcessor.processMap(map, tick) local index = map.processIndex local outgoingWave = map.outgoingScanWave local processQueue = map.processQueue local processQueueLength = #processQueue local step local endIndex if outgoingWave then step = 1 endIndex = mMin(index + PROCESS_QUEUE_SIZE, processQueueLength) else step = -1 endIndex = mMax(index - PROCESS_QUEUE_SIZE, 1) end if (processQueueLength == 0) then return end for x=index,endIndex,step do local chunk = processQueue[x] if chunk and (chunk[CHUNK_TICK] ~= tick) then chunk[CHUNK_TICK] = tick processPheromone(map, chunk) end end if (endIndex == processQueueLength) then map.outgoingScanWave = false elseif (endIndex == 1) then map.outgoingScanWave = true elseif outgoingWave then map.processIndex = endIndex + 1 else map.processIndex = endIndex - 1 end end function mapProcessor.processStaticMap(map) local index = map.processStaticIndex local outgoingWave = map.outgoingStaticScanWave local processQueue = map.processQueue local processQueueLength = #processQueue local step local endIndex if outgoingWave then step = 1 endIndex = mMin(index + PROCESS_STATIC_QUEUE_SIZE, processQueueLength) else step = -1 endIndex = mMax(index - PROCESS_STATIC_QUEUE_SIZE, 1) end if (processQueueLength == 0) then return end for x=index,endIndex,step do local chunk = processQueue[x] processStaticPheromone(map, chunk) end if (endIndex == processQueueLength) then map.outgoingStaticScanWave = false elseif (endIndex == 1) then map.outgoingStaticScanWave = true elseif outgoingWave then map.processStaticIndex = endIndex + 1 else map.processStaticIndex = endIndex - 1 end end local function queueNestSpawners(map, chunk, tick) local processActiveNest = map.universe.processActiveNest if not processActiveNest[chunk] then if (getNestActiveness(map, chunk) > 0) or (getRaidNestActiveness(map, chunk) > 0) then processActiveNest[chunk] = { map = map, tick = tick + DURATION_ACTIVE_NEST } end end end local xyVector = {{-1, -1}, {-1, 1}, {1, -1}, {1, 1}} local huntingChance = {} huntingChance[0] = 1 huntingChance[32] = 0.5 huntingChance[64] = 0.1 huntingChance[96] = 0.02 huntingChance[128] = 0.004 --[[ Localized player radius were processing takes place in realtime, doesn't store state between calls. vs the slower passive version processing the entire map in multiple passes. --]] function mapProcessor.processPlayers(players, universe, tick) -- put down player pheromone for player hunters -- randomize player order to ensure a single player isn't singled out -- not looping everyone because the cost is high enough already in multiplayer local enemyForce = game.forces["enemy"] universe.chunkToPlayerCount = {} for i=1,#players do local player = players[i] if validPlayer(player) and enemyForce.is_enemy(player.force) then local playerCharacter = player.character if playerCharacter then local map = universe.maps[playerCharacter.surface.index] if map then local chunk = getChunkByPosition(map, playerCharacter.position) if (chunk ~= -1) then universe.chunkToPlayerCount[chunk] = (universe.chunkToPlayerCount[chunk] or 0) + 1 end end end end end if (#players > 0) then local player = players[mRandom(#players)] if validPlayer(player) and enemyForce.is_enemy(player.force) then local playerCharacter = player.character if playerCharacter then local map = universe.maps[playerCharacter.surface.index] if map then local playerChunk = getChunkByPosition(map, playerCharacter.position) if (playerChunk ~= -1) then local roll = mRandom() local allowingAttacks = canAttack(map, tick) local allowingVengence = (allowingAttacks or settings.global["rampantFixed--allowDaytimePlayerHunting"].value) and (map.points >= (AI_VENGENCE_SQUAD_COST*0)) local forceVengence = (getEnemyStructureCount(map, playerChunk) > 0) for dx= 0, PROCESS_PLAYER_BOUND, 32 do for dy= 0, PROCESS_PLAYER_BOUND, 32 do local vengence = allowingVengence and (forceVengence or (roll < (huntingChance[mMax(dx, dy)] or 0.002))) for _, vector in pairs(xyVector) do local x = playerChunk.x + dx*vector[1] local y = playerChunk.y - dy*vector[2] local chunk = getChunkByXY(map, x, y) if (chunk ~= -1) and (chunk[CHUNK_TICK] ~= tick) then chunk[CHUNK_TICK] = tick processPheromone(map, chunk, true) if (not chunk.nextHuntTick) or (chunk.nextHuntTick <= tick) then if (getNestCount(map, chunk) > 0) then processNestActiveness(map, chunk) --queueNestSpawners(map, chunk, tick) -- + !КДА too high cost. Lets just set "active" if vengence then local count = map.vengenceQueue[chunk] if not count then count = 0 map.vengenceQueue[chunk] = count end map.vengenceQueue[chunk] = count + 1 chunk.nextHuntTick = tick + COOLDOWN_HUNT end end end end --- compressed squads if (chunk ~= -1) then for _,squad in pairs(getSquadsOnChunk(map, chunk)) do local unitGroup = squad.group if squad.compressed and unitGroup and unitGroup.valid then squadDecompress(universe, map.surface, squad, nil, playerCharacter, true) end end end --- compressed squads end end end end end end end end end function mapProcessor.cleanUpMapTables(map, tick) local index = map.cleanupIndex local retreats = map.chunkToRetreats local rallys = map.chunkToRallys local drained = map.chunkToDrained local processQueue = map.processQueue local processQueueLength = #processQueue local endIndex = mMin(index + CLEANUP_QUEUE_SIZE, processQueueLength) if (processQueueLength == 0) then return end for x=index,endIndex do local chunk = processQueue[x] local retreatTick = retreats[chunk] if retreatTick and ((tick - retreatTick) > COOLDOWN_RETREAT) then retreats[chunk] = nil end local rallyTick = rallys[chunk] if rallyTick and ((tick - rallyTick) > COOLDOWN_RALLY) then rallys[chunk] = nil end local drainTick = drained[chunk] if drainTick and ((tick - drainTick) > 0) then drained[chunk] = nil end end if (endIndex == processQueueLength) then map.cleanupIndex = 1 else map.cleanupIndex = endIndex + 1 end end --[[ Passive scan to find entities that have been generated outside the factorio event system --]] function mapProcessor.scanPlayerMap(map, tick) if (map.nextProcessMap == tick) or (map.nextPlayerScan == tick) or (map.nextEnemyScan == tick) or (map.nextChunkProcess == tick) then return end local index = map.scanPlayerIndex local area = map.universe.area local offset = area[2] local chunkBox = area[1] local processQueue = map.processQueue local processQueueLength = #processQueue local endIndex = mMin(index + PLAYER_QUEUE_SIZE, processQueueLength) if (processQueueLength == 0) then return end for x=index,endIndex do local chunk = processQueue[x] chunkBox[1] = chunk.x chunkBox[2] = chunk.y offset[1] = chunk.x + CHUNK_SIZE offset[2] = chunk.y + CHUNK_SIZE mapScanPlayerChunk(chunk, map) end if (endIndex == processQueueLength) then map.scanPlayerIndex = 1 else map.scanPlayerIndex = endIndex + 1 end end function mapProcessor.scanEnemyMap(map, tick) if (map.nextProcessMap == tick) or (map.nextPlayerScan == tick) or (map.nextChunkProcess == tick) then return end local index = map.scanEnemyIndex local area = map.universe.area local offset = area[2] local chunkBox = area[1] local processQueue = map.processQueue local processQueueLength = #processQueue local endIndex = mMin(index + ENEMY_QUEUE_SIZE, #processQueue) if (processQueueLength == 0) then return end for x=index,endIndex do local chunk = processQueue[x] chunkBox[1] = chunk.x chunkBox[2] = chunk.y offset[1] = chunk.x + CHUNK_SIZE offset[2] = chunk.y + CHUNK_SIZE mapScanEnemyChunk(chunk, map) end if (endIndex == processQueueLength) then map.scanEnemyIndex = 1 else map.scanEnemyIndex = endIndex + 1 end end function mapProcessor.scanResourceMap(map, tick) if (map.nextProcessMap == tick) or (map.nextPlayerScan == tick) or (map.nextEnemyScan == tick) or (map.nextChunkProcess == tick) then return end local index = map.scanResourceIndex local area = map.universe.area local offset = area[2] local chunkBox = area[1] local processQueue = map.processQueue local processQueueLength = #processQueue local endIndex = mMin(index + RESOURCE_QUEUE_SIZE, processQueueLength) if (processQueueLength == 0) then return end for x=index,endIndex do local chunk = processQueue[x] chunkBox[1] = chunk.x chunkBox[2] = chunk.y offset[1] = chunk.x + CHUNK_SIZE offset[2] = chunk.y + CHUNK_SIZE mapScanResourceChunk(chunk, map) end if (endIndex == processQueueLength) then map.scanResourceIndex = 1 else map.scanResourceIndex = endIndex + 1 end end function mapProcessor.processActiveNests(universe, tick) local processActiveNest = universe.processActiveNest local chunk = universe.processActiveNestIterator local chunkPack if not chunk then chunk, chunkPack = next(processActiveNest, nil) else chunkPack = processActiveNest[chunk] end if not chunk then universe.processActiveNestIterator = nil else universe.processActiveNestIterator = next(processActiveNest, chunk) if chunkPack.tick < tick then local map = chunkPack.map if (not map) or (not map.surface.valid) then processActiveNest[chunk] = nil return end processNestActiveness(map, chunk) if (getNestActiveness(map, chunk) == 0) and (getRaidNestActiveness(map, chunk) == 0) then processActiveNest[chunk] = nil else chunkPack.tick = tick + DURATION_ACTIVE_NEST end end end end function mapProcessor.processVengence(map) local ss = map.vengenceQueue local chunk = map.deployVengenceIterator if not chunk then chunk = next(ss, nil) end if not chunk then map.deployVengenceIterator = nil if (tableSize(ss) == 0) then map.vengenceQueue = {} end else map.deployVengenceIterator = next(ss, chunk) formVengenceSquad(map, chunk) ss[chunk] = nil end end function mapProcessor.processNests(map, tick) local bases = map.chunkToBase local chunks = map.chunkToNests local chunk = next(chunks, map.processNestIterator) if not chunk then map.processNestIterator = nil return else processNestActiveness(map, chunk) queueNestSpawners(map, chunk, tick) if map.universe.NEW_ENEMIES then local base = bases[chunk] if base and base.thisIsRampantEnemy and ((tick - base.tick) > BASE_PROCESS_INTERVAL) then processBase(chunk, map, tick, base) end end end map.processNestIterator = chunk end local function chooseSquad(map, migrate, attack) local settlementsProbability = settings.global["rampantFixed--settlementsProbability"].value if migrate and ((map.activeRaidNests == 0) or (mRandom()= map.universe.AI_MAX_BUILDER_COUNT then currentSquad = "attack" end elseif migrate and (map.universe.squadCount >= (map.universe.AI_MAX_SQUAD_COUNT*0.8)) then currentSquad = "migrate" end local squadCreated = false if remoteInterfaceParameters then currentSquad = "attack" squadCreated = formSquads(map, chunk, tick, remoteInterfaceParameters) elseif migrate and (currentSquad == "migrate") then squadCreated = formSettlers(map, chunk) elseif attack then squadCreated = formSquads(map, chunk, tick) end if squadCreated then if remoteInterfaceParameters then if settings.global["rampantFixed--aiPointsPrintSpendingToChat"].value then game.print(currentSquad.." squad created by remote interface") end else map.nextSquad = chooseSquad(map, migrate, attack) if settings.global["rampantFixed--aiPointsPrintSpendingToChat"].value then game.print(currentSquad.." squad created. Next is "..map.nextSquad.." squad") end end end end map[iterator] = chunk end function mapProcessor.processSpawners(map, tick, remoteInterfaceParameters) if (map.state ~= AI_STATE_PEACEFUL) then if (map.state == AI_STATE_MIGRATING) then if remoteInterfaceParameters or (map.nextSquad and (map.nextSquad == "attack")) then processSpawners(map, tick, "processActiveRaidSpawnerIterator", map.chunkToActiveRaidNest , remoteInterfaceParameters ) else processSpawners(map, tick, "processMigrationIterator", map.chunkToNests , remoteInterfaceParameters ) end elseif (map.state == AI_STATE_AGGRESSIVE) or (map.state == AI_STATE_GROWING) then if map.universe.raidAIToggle and (mRandom() < 0.2) then processSpawners(map, tick, "processActiveRaidSpawnerIterator", map.chunkToActiveRaidNest , remoteInterfaceParameters ) else processSpawners(map, tick, "processActiveSpawnerIterator", map.chunkToActiveNest , remoteInterfaceParameters ) end elseif (map.state == AI_STATE_SIEGE) then processSpawners(map, tick, "processActiveSpawnerIterator", map.chunkToActiveNest , remoteInterfaceParameters ) processSpawners(map, tick, "processActiveRaidSpawnerIterator", map.chunkToActiveRaidNest , remoteInterfaceParameters ) processSpawners(map, tick, "processMigrationIterator", map.chunkToNests , remoteInterfaceParameters ) else processSpawners(map, tick, "processActiveSpawnerIterator", map.chunkToActiveNest , remoteInterfaceParameters ) processSpawners(map, tick, "processActiveRaidSpawnerIterator", map.chunkToActiveRaidNest , remoteInterfaceParameters ) end end end local querySpawners = {area={}, force="enemy", type={"turret", "unit-spawner"}} -- growType = 0 - center local function growChunk(map, base, chunk, growType) local growCenter if growType == 0 then local universe = map.universe querySpawners.area = {{chunk.x, chunk.y}, {chunk.x + 32, chunk.y + 32}} local entities = map.surface.find_entities_filtered(querySpawners) if #entities == 0 then return false end newEntity = baseUtils.upgradeEntity(entities[mRandom(#entities)], base.alignment, map, nil, true, true) if newEntity and newEntity.valid then chunk.growFails = nil registerEnemyBaseStructure(map, newEntity, base) -- game.print("Growing chunk: [gps=" .. newEntity.position.x .. "," .. newEntity.position.y .."] ".. newEntity.name) -- debug return true else chunk.growFails = (chunk.growFails or 0) + 1 -- game.print("Growing chunk: [gps=" .. chunk.x .. "," .. chunk.y .."], can't place "..chunk.growFails.."/5") -- debug end end return false end function mapProcessor.processGrowingBases(universe, tick) local growingBases = universe.growingBases local baseId if universe.growingBasesIterator and growingBases[universe.growingBasesIterator] then baseId = next(growingBases, nil) else baseId = next(growingBases, universe.growingBasesIterator) end if not baseId then universe.growingBasesIterator = nil return end local growingData = growingBases[baseId] if not growingData then return end local base = universe.bases[baseId] if not base then universe.growingBases[baseId] = nil universe.growingBasesIterator = nil return end local map = universe.maps[base.mapIndex] if not map then universe.growingBases[baseId] = nil universe.growingBasesIterator = nil return end local surface = map.surface if not surface then return end -- if map.state ~= AI_STATE_GROWING then -- return -- end universe.growingBasesIterator = baseId if growingData.tick > tick then return end local growed = false local chunksCount = 0 local activeChunks = 0 local chunksArray = {} for chunk, _ in pairs(base.chunks) do local nestCount = getNestCount(map, chunk) + getHiveCount(map, chunk) local turretCount = getTurretCount(map, chunk) chunksCount = chunksCount + 1 if (getNestActiveness(map, chunk) > 0) then activeChunks = activeChunks + 1 end if baseUtils.chunkCanGrow(map, chunk) then chunksArray[#chunksArray+1] = chunk -- if not growed then -- -- if growingData.nests and (growingData.nests>0) then -- -- growChunk(map, base, chunk, 1) -- -- growingData.nests = growingData.nests - 1 -- -- elseif growingData.worms and (growingData.worms>0) then -- -- growChunk(map, base, chunk, 2) -- -- growingData.worms = growingData.worms - 1 -- -- else -- growed = growChunk(map, base, chunk, 0) -- -- end -- end surface.create_trivial_smoke({name = "growing-dust-nonTriggerCloud-rampant", position = {chunk.x + 16, chunk.y + 16}}) end end if #chunksArray > 0 then growed = growChunk(map, base, chunksArray[mRandom(#chunksArray)], 0) end if not growed then base.growFails = (base.growFails or 0) + 1 -- game.print("base fail to grow: #"..base.id.." try # = "..base.growFails) -- debug if (base.growFails > 5) or (activeChunks == 0) or (chunksCount >= 6) then -- game.print("base stop growing: #"..base.id.." activeChunks = "..activeChunks..", growFails =".. base.growFails..", chunksCount = "..chunksCount) -- debug universe.growingBases[baseId] = nil universe.growingBasesIterator = nil end else base.growFails = nil end growingData.tick = tick + GROWING_COOLDOWN end function mapProcessor.suspendClearedMaps(universe, tick) local map if not universe.suspendMapsIterator then universe.suspendMapsIterator, map = next(universe.maps, nil) else map = universe.maps[universe.suspendMapsIterator] end if (not map) or (not map.surface) then if map and (not map.suspended) then map.suspended = true map.suspendCheckTick = tick end universe.suspendMapsIterator, map = next(universe.maps, universe.suspendMapsIterator) return end if map.surface.name == "nauvis" then else if not map.suspended then if (map.activeRaidNests < 1) and (tick - (map.suspendCheckTick or 0) > 3600)then local enemySpawners = map.surface.count_entities_filtered({force = "enemy", type = "unit-spawner", limit = 1}) if enemySpawners == 0 then map.suspended = true --game.print("suspend clear surface:"..map.surface.name) -- debug map.suspended end map.suspendCheckTick = tick end end end universe.suspendMapsIterator, map = next(universe.maps, universe.suspendMapsIterator) end mapProcessorG = mapProcessor return mapProcessor