470 lines
18 KiB
Lua
470 lines
18 KiB
Lua
-- Suggestions for additional functionality are welcome
|
|
-- look in RampantFixedRemote\remote_interface_example.lua for some tips
|
|
|
|
-- now you can:
|
|
-- get/set maximum wave size: wave sizes are proportional to this value
|
|
-- get/set wave size: specifies the size of the waves directly and disables its recalculation
|
|
-- get array of LuaUnitGroup for rampant squads (only attack squads)
|
|
-- order to create an attack squad of a specified size or a default size.
|
|
-- get array of all enemy bases stats, or get enemy base stats for specified position / specified Id
|
|
-- directly set enemy base factions
|
|
-- get/set AI points and AI state for specified surface
|
|
-- get pheromones for specified chunk
|
|
-- set enemy base to grow
|
|
|
|
-- setting parameters requires permission. Don't forget to check it out
|
|
-- all functions *_ExtCtrl do nothing and return nil, if startup setting "rampantFixed--allowExternalControl" (Remote interface: allow external control AI) disabled -- upd. 1.8.3+ "allowExternalControl" always true
|
|
-- it is recommended to issue a message to the user to enable setting
|
|
|
|
local aiPlanning = require("libs/AIPlanning")
|
|
local baseUtils = require("libs/BaseUtils")
|
|
local chunkPropertyUtils = require("libs/ChunkPropertyUtils")
|
|
local constants = require("Constants")
|
|
local mapProcessor = require("libs/MapProcessor")
|
|
local mapUtils = require("libs/MapUtils")
|
|
local config = require("__RampantFixed__/config")
|
|
|
|
local function getBaseStats(base)
|
|
if not base then
|
|
return nil
|
|
end
|
|
local baseStats = {
|
|
id = base.id, -- number
|
|
mapIndex = base.mapIndex, -- its same as surface.index
|
|
x = base.x, -- 1st chunk, where base has been created. x, y coordinates
|
|
y = base.y,
|
|
thisIsRampantEnemy = base.thisIsRampantEnemy, -- boolean. Only if true, base will mutate
|
|
tierHandicap = base.tierHandicap, -- 0..2. Lesser than base.tier. Actual tier = base.tier - base.tierHandicap.
|
|
tier = base.tier, -- number 1..10
|
|
factions = {}, -- {factionName1 = number, ..., factionNameN = number }. Sum of numbers must be equal 1
|
|
chunks = {} -- list of base chunks coordinates (upper left corner) {{x = number, y = number}, ... ...,{x = number, y = number}}
|
|
}
|
|
local alignment
|
|
if base.newAlignmentAndSteps then
|
|
alignment = base.newAlignmentAndSteps[2]
|
|
else
|
|
alignment = base.alignment
|
|
end
|
|
|
|
for faction, rate in pairs(alignment) do
|
|
baseStats.factions[faction] = rate
|
|
end
|
|
|
|
for chunk, _ in pairs(base.chunks) do
|
|
if chunk and (type(chunk) == "table") then
|
|
baseStats.chunks[#baseStats.chunks + 1] = {x = chunk.x, y = chunk.y}
|
|
end
|
|
end
|
|
return baseStats
|
|
end
|
|
|
|
local function getFactionsList()
|
|
local factions = {}
|
|
for i=1,#constants.FACTION_SET do
|
|
local faction = constants.FACTION_SET[i]
|
|
factions[faction.type] = {tierMin = faction.acceptRate[1], tierMax = faction.acceptRate[2]}
|
|
end
|
|
return factions
|
|
end
|
|
|
|
local validAI_StateList = {}
|
|
validAI_StateList[constants.AI_STATE_PEACEFUL] = constants.stateEnglish[constants.AI_STATE_PEACEFUL] -- [1]
|
|
validAI_StateList[constants.AI_STATE_AGGRESSIVE] = constants.stateEnglish[constants.AI_STATE_AGGRESSIVE] -- [2]
|
|
validAI_StateList[constants.AI_STATE_RAIDING] = constants.stateEnglish[constants.AI_STATE_RAIDING] -- [4] -- its not typo. state 3 isn't exist
|
|
validAI_StateList[constants.AI_STATE_MIGRATING] = constants.stateEnglish[constants.AI_STATE_MIGRATING] -- [5]
|
|
validAI_StateList[constants.AI_STATE_SIEGE] = constants.stateEnglish[constants.AI_STATE_SIEGE] -- [6]
|
|
validAI_StateList[constants.AI_STATE_ONSLAUGHT] = constants.stateEnglish[constants.AI_STATE_ONSLAUGHT] -- [7]
|
|
validAI_StateList[constants.AI_STATE_GROWING] = constants.stateEnglish[constants.AI_STATE_GROWING] -- [8]
|
|
|
|
|
|
remote.add_interface("rampantFixed", {
|
|
--------------------
|
|
-- return true if external control is enabled
|
|
-- note: all functions *_ExtCtrl do nothing and return nil, if allowExternalControl = false
|
|
-- note: since the "rampantFixed--allowExternalControl" is a startup setting, it is enough to check it once (on every load)
|
|
allowExternalControl = function()
|
|
return global.universe.allowExternalControl
|
|
end
|
|
,
|
|
--------------------
|
|
-- set maximum wave size. Canceled if no value is passed, or a value outside the allowed range.
|
|
-- parameters: {attackWaveMaxSize = number}
|
|
-- return: number
|
|
setWaveMaxSize_ExtCtrl = function(parameters)
|
|
if not global.universe.allowExternalControl then
|
|
return nil
|
|
end
|
|
if parameters and parameters.attackWaveMaxSize and (type(parameters.attackWaveMaxSize) == "number") and (parameters.attackWaveMaxSize > 0) and (parameters.attackWaveMaxSize <= 1000) then
|
|
global.universe.externalControlValues.attackWaveMaxSize = parameters.attackWaveMaxSize
|
|
else
|
|
global.universe.externalControlValues.attackWaveMaxSize = nil
|
|
end
|
|
aiPlanning.planningUniverse(global.universe, game.forces.enemy.evolution_factor, game.tick)
|
|
return config.getAttackWaveMaxSize(global.universe)
|
|
end
|
|
,
|
|
--------------------
|
|
-- return: number of maximum wave size
|
|
-- note: Based on maximum wave size, the wave size is calculated
|
|
getWaveMaxSize = function()
|
|
return config.getAttackWaveMaxSize(global.universe)
|
|
end
|
|
,
|
|
--------------------
|
|
-- set and freeze wave size. Canceled if no value is passed, or a value outside the allowed range
|
|
-- parameters: {attackWaveSize = number}
|
|
-- return: number
|
|
-- note: its average wave size, including all modifiers. When forming attacking squads, this value is taken
|
|
-- note: starting from 50% of the maximum size, the cost of the squad increases
|
|
setWaveSize_ExtCtrl = function(parameters)
|
|
if not global.universe.allowExternalControl then
|
|
return nil
|
|
end
|
|
if parameters and parameters.attackWaveSize and (type(parameters.attackWaveSize) == "number") and (parameters.attackWaveSize > 0) and (parameters.attackWaveSize <= 1000) then
|
|
global.universe.externalControlValues.attackWaveSize = parameters.attackWaveSize
|
|
else
|
|
global.universe.externalControlValues.attackWaveSize = nil
|
|
end
|
|
aiPlanning.planningUniverse(global.universe, game.forces.enemy.evolution_factor, game.tick)
|
|
return config.getAttackWaveSize(global.universe)
|
|
end
|
|
,
|
|
--------------------
|
|
-- return: number
|
|
-- note: its average wave size, including all modifiers. When forming attacking squads, this value is taken
|
|
getWaveSize = function()
|
|
return config.getAttackWaveSize(global.universe)
|
|
end
|
|
,
|
|
--------------------
|
|
-- return: array of luaUnitGroup. All surfaces. Only Rampant squads. Settlers and underground attacks are not in this list
|
|
getRampantAttackGroups = function()
|
|
local groups = {}
|
|
for groupNumber, squad in pairs(global.universe.groupNumberToSquad) do
|
|
if squad.map and squad.group.valid then
|
|
groups[#groups+1] = squad.group
|
|
end
|
|
end
|
|
return groups
|
|
end
|
|
,
|
|
--------------------
|
|
-- Creates an attack squad based on the current AI mode. Won't spawn a squad if peaceful mode, no suitable nests, or no biters near nests or no player base
|
|
-- It is not recommended to call many times per tick: the procedure for gathering a squad is resource-intensive.
|
|
-- A newly created squad must first gather at a rally point before it is ready to attack.
|
|
-- If the AI does not have enough action points, then squad will be free
|
|
--
|
|
-- parameters: {surfaceIndex = number, ignoreSquadLimit = bool, size = nil or numeric}
|
|
-- ignoreSquadLimit - allow to create more squads then "rampantFixed--maxNumberOfSquads"
|
|
-- size (optional)- number of squad members. If not specified, the default squad size is used. If there are no so many biters, then the actual size will be smaller
|
|
--
|
|
-- return: LuaUnitGroup or nil, if cant create squad
|
|
createSquad_ExtCtrl = function(parameters)
|
|
if not global.universe.allowExternalControl then
|
|
return nil
|
|
end
|
|
if (not parameters) or (not parameters.surfaceIndex) then
|
|
return nil
|
|
end
|
|
local map = global.universe.maps[parameters.surfaceIndex]
|
|
if not map then
|
|
return nil
|
|
end
|
|
local remoteInterfaceParameters
|
|
if parameters.size and (type(parameters.size) == "number") then
|
|
remoteInterfaceParameters = {ignoreSquadLimit = parameters.ignoreSquadLimit, size = parameters.size}
|
|
else
|
|
remoteInterfaceParameters = {ignoreSquadLimit = parameters.ignoreSquadLimit}
|
|
end
|
|
|
|
for i = 1, 10 do
|
|
remoteInterfaceParameters.i = i
|
|
mapProcessor.processSpawners(map, game.tick, remoteInterfaceParameters)
|
|
if remoteInterfaceParameters.squad then
|
|
return remoteInterfaceParameters.squad.group
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
,
|
|
--------------------
|
|
-- parameters: {surfaceIndex = number, position = {x = number , y = number}}
|
|
--
|
|
-- return: table of base stats or nil, if no base
|
|
-- see the list of props in the "local function getBaseStats "
|
|
getBaseByPosition = function(parameters)
|
|
if (not parameters) or (not parameters.surfaceIndex) then
|
|
return nil
|
|
end
|
|
local map = global.universe.maps[parameters.surfaceIndex]
|
|
if not map then
|
|
return nil
|
|
end
|
|
if not parameters.position then
|
|
return nil
|
|
end
|
|
|
|
local chunk = mapUtils.getChunkByPosition(map, parameters.position)
|
|
if (chunk == -1) then
|
|
return nil
|
|
end
|
|
local base = chunkPropertyUtils.getChunkBase(map, chunk)
|
|
if not base then
|
|
return nil
|
|
end
|
|
return getBaseStats(base)
|
|
end
|
|
,
|
|
--------------------
|
|
-- parameters: {id = number}
|
|
-- return: table of base stats or nil, if no base
|
|
getBaseById = function(parameters)
|
|
if parameters and parameters.id then
|
|
return getBaseStats(global.universe.bases[parameters.id])
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
,
|
|
--------------------
|
|
-- return array of bases stats (look at getBaseStats)
|
|
getBases = function()
|
|
local bases = {}
|
|
for i, base in pairs(global.universe.bases) do
|
|
bases[#bases+1] = getBaseStats(base)
|
|
end
|
|
return bases
|
|
end
|
|
,
|
|
--------------------
|
|
-- Returns a factions based on the mod settings (players can disable factions in the launch options)
|
|
-- return: table {
|
|
-- factionName1 = {tierMin = Number, tierMax = Number},
|
|
-- ...,
|
|
-- factionNameN = ...}
|
|
getFactions = function()
|
|
return getFactionsList()
|
|
end
|
|
,
|
|
--------------------
|
|
-- Returns a factions for specified tier.
|
|
-- parameters: {tier = Number 1..10}
|
|
-- return: table {
|
|
-- factionName1 = {tierMin = Number, tierMax = Number},
|
|
-- ...,
|
|
-- factionNameN = ...}
|
|
getFactionsByTier = function(parameters)
|
|
local factions = {}
|
|
if parameters and parameters.tier and (type(parameters.tier) == "number") then
|
|
for i=1,#constants.FACTION_SET do
|
|
local faction = constants.FACTION_SET[i]
|
|
if (faction.acceptRate[1]<=parameters.tier) and faction.acceptRate[2]>=parameters.tier then
|
|
factions[faction.type] = {tierMin = faction.acceptRate[1], tierMax = faction.acceptRate[2]}
|
|
end
|
|
end
|
|
end
|
|
return factions
|
|
end
|
|
|
|
,
|
|
--------------------
|
|
-- Assigns a set of factions to the base. The mutation will happen by itself, after some time
|
|
--
|
|
-- parameters: {id = number, factions = {factionName1 = rate1, factionName2 = rate2}}
|
|
-- sum of rates must be = 1
|
|
-- The number of factions should preferably be equal 2, although 1 or 3+ will also work.
|
|
-- return:
|
|
-- -1 if no parameters or wrong parameters (wrong base id, missed "factions", etc...)
|
|
-- -2 if sum of rates not equals 1
|
|
-- 1 if set is successful
|
|
--
|
|
-- note: missed factions will be write as "neutral" faction
|
|
-- note: it can take up to 10 minutes between the installation of new genes and the actual mutation. Depends
|
|
-- on the size of the map and how long ago there was a mutation in the chunk. Mutation is not possible more than once every 10 minutes
|
|
-- also, if there is already a mass mutation in the chunk at the time the command is issued, then the delay will increase
|
|
setBaseFactions_ExtCtrl = function(parameters)
|
|
if not global.universe.allowExternalControl then
|
|
return nil
|
|
end
|
|
if (not parameters) or (not parameters.id) then
|
|
return -1
|
|
end
|
|
local base = global.universe.bases[parameters.id]
|
|
if not base then
|
|
return -1
|
|
end
|
|
if (not parameters) or (not parameters.factions) or (not type(parameters.factions) == "table") then
|
|
return -1
|
|
end
|
|
-- lets check new factions for mistakes
|
|
local factionsList = getFactionsList()
|
|
local sumRates = 0
|
|
local baseFactions = {}
|
|
for faction, rate in pairs(parameters.factions) do
|
|
if (type(faction) == "string") and (type(rate) == "number") then
|
|
sumRates = sumRates + rate
|
|
if factionsList[faction] then
|
|
baseFactions[faction] = (baseFactions[faction] or 0) + rate
|
|
else
|
|
baseFactions["neutral"] = (baseFactions["neutral"] or 0) + rate
|
|
end
|
|
end
|
|
end
|
|
|
|
if (sumRates >= 0.99) and (sumRates <= 1.01) then
|
|
local tick = game.tick
|
|
base.newAlignmentAndSteps = nil
|
|
base.alignment = baseFactions
|
|
changingEntities = true
|
|
base.nextMutationTick = tick + 54000 -- prevent ingame mutations for 15 min
|
|
for chunk, _ in pairs(base.chunks) do
|
|
chunk.nextMutationTick = tick
|
|
end
|
|
return 1
|
|
else
|
|
return -2
|
|
end
|
|
end
|
|
,
|
|
--------------------
|
|
-- parameters: {surfaceIndex = number}
|
|
-- return: nil or number of points
|
|
getAI_points = function(parameters)
|
|
if (not parameters) or (not parameters.surfaceIndex) then
|
|
return nil
|
|
end
|
|
local map = global.universe.maps[parameters.surfaceIndex]
|
|
if not map then
|
|
return nil
|
|
end
|
|
return map.points
|
|
end
|
|
|
|
,
|
|
--------------------
|
|
-- set AI point for specified surface. Return nil, if no surface, or surface is ignored by Rampant, or rampantFixed--allowExternalControl is disabled
|
|
-- set points if parameters.points defined
|
|
-- parameters: {surfaceIndex = number, points = number}
|
|
-- return: nil or number of points
|
|
-- note: attack squad cost 175 points (can vary from 90 to 350, depending on various modifiers). But it is worth focusing on the value 175
|
|
setAI_points_ExtCtrl = function(parameters)
|
|
if not global.universe.allowExternalControl then
|
|
return nil
|
|
end
|
|
if (not parameters) or (not parameters.surfaceIndex) then
|
|
return nil
|
|
end
|
|
local map = global.universe.maps[parameters.surfaceIndex]
|
|
if not map then
|
|
return nil
|
|
end
|
|
if parameters.points then
|
|
if parameters.points <= 0 then
|
|
map.points = 0
|
|
elseif parameters.points > 100000 then
|
|
map.points = 100000
|
|
else
|
|
map.points = parameters.points
|
|
end
|
|
end
|
|
return map.points
|
|
end
|
|
,
|
|
--------------------
|
|
-- set AI state for specified surface. Return nil, if no surface, or surface is ignored by Rampant, or rampantFixed--allowExternalControl is disabled
|
|
-- parameters: {surfaceIndex = number, state = number}
|
|
-- return: nil or number (new AI state)
|
|
setAI_state_ExtCtrl = function(parameters)
|
|
if not global.universe.allowExternalControl then
|
|
return nil
|
|
end
|
|
if (not parameters) or (not parameters.surfaceIndex) then
|
|
return nil
|
|
end
|
|
local map = global.universe.maps[parameters.surfaceIndex]
|
|
if not map then
|
|
return nil
|
|
end
|
|
if (not parameters.state) or (not validAI_StateList[parameters.state]) then
|
|
return nil
|
|
end
|
|
|
|
map.state = parameters.state
|
|
return {state = map.state, stateEnglish = validAI_StateList[map.state]}
|
|
end
|
|
,
|
|
--------------------
|
|
-- parameters: {surfaceIndex = number}
|
|
-- return: nil or number
|
|
getAI_state = function(parameters)
|
|
if (not parameters) or (not parameters.surfaceIndex) then
|
|
return nil
|
|
end
|
|
local map = global.universe.maps[parameters.surfaceIndex]
|
|
if not map then
|
|
return nil
|
|
end
|
|
return {state = map.state, stateEnglish = validAI_StateList[map.state]}
|
|
end
|
|
,
|
|
--------------------
|
|
-- return: table {
|
|
-- 1 = "AI_STATE_PEACEFUL",
|
|
-- 2 =
|
|
-- ..
|
|
-- }
|
|
getValidAI_StateList = function()
|
|
return validAI_StateList
|
|
end
|
|
|
|
,
|
|
--------------------
|
|
-- Returns a pheromones in specificed chunk
|
|
-- parameters: {surfaceIndex = number, position = {x = number , y = number}}
|
|
-- return: table {BASE_PHEROMONE = number, BASE_DETECTION_PHEROMONE = number, PLAYER_PHEROMONE = number, RESOURCE_PHEROMONE = number} (or nil if surface ignored or chunk isn't generated or not processed yet)
|
|
getPheromones = function(parameters)
|
|
if (not parameters) or (not parameters.surfaceIndex) then
|
|
return nil
|
|
end
|
|
local map = global.universe.maps[parameters.surfaceIndex]
|
|
if not map then
|
|
return nil
|
|
end
|
|
|
|
if not parameters.position then
|
|
return nil
|
|
end
|
|
local chunk = mapUtils.getChunkByPosition(map, parameters.position)
|
|
if (chunk == -1) then
|
|
return nil
|
|
end
|
|
local pheromones = {}
|
|
pheromones["BASE_PHEROMONE"] = chunk[constants.BASE_PHEROMONE] or 0 -- most attacks go the way of increasing this pheromone. Reduced by 10% per chunk. Hugely reduced when passing through a chunk with a lot of defense. A large number of losses also reduces this value
|
|
pheromones["BASE_DETECTION_PHEROMONE"] = chunk[constants.BASE_DETECTION_PHEROMONE] or 0 -- max value 10000. Reduced by 10% per chunk. Military biuldings set BASE_DETECTION_PHEROMONE to 10000. Other buildings are different. If there are few buildings, then there will be less than 10000
|
|
pheromones["PLAYER_PHEROMONE"] = chunk[constants.PLAYER_PHEROMONE] or 0 -- max value 300, if player(s) inside this chunk (and no defence). Reduced by 55% per chunk
|
|
pheromones["RESOURCE_PHEROMONE"] = chunk[constants.RESOURCE_PHEROMONE] or 0 -- normalized resource estimate. Find out empirically.
|
|
-- note: pheromones are weakened more if the chunk is poorly passable.
|
|
-- pheromones are not transmitted from an adjacent chunk if the rampant thinks there is no passage
|
|
-- pheromones also spread diagonally
|
|
-- Keep in mind that pheromones do not spread immediately, and the situation on the map changes
|
|
return pheromones
|
|
end
|
|
-- ,
|
|
-- setBaseToGrow_ExtCtrl = function(parameters)
|
|
-- local universe = global.universe
|
|
-- if not universe.allowExternalControl then
|
|
-- return nil
|
|
-- end
|
|
-- if (not parameters) or (not parameters.id) then
|
|
-- return -1
|
|
-- end
|
|
-- local base = universe.bases[parameters.id]
|
|
-- if not base then
|
|
-- return -1
|
|
-- end
|
|
|
|
-- universe.growingBases[base.id] = {tick = 0, nests = 5, worms = 5}
|
|
-- return 1
|
|
-- end
|
|
}
|
|
) |