1
0
mirror of https://github.com/veden/Rampant.git synced 2025-01-20 02:59:47 +02:00
Rampant/control.lua

526 lines
19 KiB
Lua
Raw Normal View History

-- imports
2017-01-19 21:58:36 -08:00
local mapUtils = require("libs/MapUtils")
2016-08-06 20:38:47 -07:00
local unitGroupUtils = require("libs/UnitGroupUtils")
local chunkProcessor = require("libs/ChunkProcessor")
2017-05-19 00:47:24 -07:00
local baseProcessor = require("libs/BaseProcessor")
local mapProcessor = require("libs/MapProcessor")
local constants = require("libs/Constants")
local pheromoneUtils = require("libs/PheromoneUtils")
local squadDefense = require("libs/SquadDefense")
local squadAttack = require("libs/SquadAttack")
local aiAttackWave = require("libs/AIAttackWave")
2016-10-14 17:00:18 -07:00
local aiPlanning = require("libs/AIPlanning")
2017-05-06 02:03:28 -07:00
local interop = require("libs/Interop")
2016-08-21 15:59:17 -07:00
local tests = require("tests")
2017-11-20 23:27:03 -08:00
local chunkUtils = require("libs/ChunkUtils")
2017-05-19 00:47:24 -07:00
local upgrade = require("Upgrade")
2017-05-31 18:46:53 -07:00
local mathUtils = require("libs/MathUtils")
2017-06-01 00:03:07 -07:00
local config = require("config")
2016-10-14 17:00:18 -07:00
-- constants
local INTERVAL_LOGIC = constants.INTERVAL_LOGIC
local INTERVAL_PROCESS = constants.INTERVAL_PROCESS
local INTERVAL_CHUNK = constants.INTERVAL_CHUNK
local INTERVAL_SCAN = constants.INTERVAL_SCAN
local INTERVAL_SQUAD = constants.INTERVAL_SQUAD
2016-10-14 17:00:18 -07:00
2017-01-19 21:58:36 -08:00
local MOVEMENT_PHEROMONE = constants.MOVEMENT_PHEROMONE
2017-11-20 23:27:03 -08:00
local SENTINEL_IMPASSABLE_CHUNK = constants.SENTINEL_IMPASSABLE_CHUNK
2017-08-08 01:19:51 -07:00
2017-06-01 00:03:07 -07:00
local AI_MAX_OVERFLOW_POINTS = constants.AI_MAX_OVERFLOW_POINTS
local RETREAT_GRAB_RADIUS = constants.RETREAT_GRAB_RADIUS
local RETREAT_SPAWNER_GRAB_RADIUS = constants.RETREAT_SPAWNER_GRAB_RADIUS
2017-05-31 18:46:53 -07:00
-- imported functions
2017-01-19 21:58:36 -08:00
2017-05-31 18:46:53 -07:00
local roundToNearest = mathUtils.roundToNearest
2017-01-19 21:58:36 -08:00
-- imported functions
2017-01-19 21:58:36 -08:00
local getChunkByPosition = mapUtils.getChunkByPosition
2018-01-02 10:36:23 -08:00
local entityForPassScan = chunkUtils.entityForPassScan
local processPendingChunks = chunkProcessor.processPendingChunks
local processScanChunks = chunkProcessor.processScanChunks
local processMap = mapProcessor.processMap
2016-10-14 17:00:18 -07:00
local processPlayers = mapProcessor.processPlayers
2016-08-28 17:05:28 -07:00
local scanMap = mapProcessor.scanMap
2016-10-14 17:00:18 -07:00
local planning = aiPlanning.planning
local rallyUnits = aiAttackWave.rallyUnits
2017-01-19 21:58:36 -08:00
local deathScent = pheromoneUtils.deathScent
2017-04-21 16:14:04 -07:00
local victoryScent = pheromoneUtils.victoryScent
local cleanSquads = unitGroupUtils.cleanSquads
local regroupSquads = unitGroupUtils.regroupSquads
local convertUnitGroupToSquad = unitGroupUtils.convertUnitGroupToSquad
local squadsAttack = squadAttack.squadsAttack
local squadsBeginAttack = squadAttack.squadsBeginAttack
local retreatUnits = squadDefense.retreatUnits
2017-11-20 23:27:03 -08:00
local addRemovePlayerEntity = chunkUtils.addRemovePlayerEntity
local unregisterEnemyBaseStructure = chunkUtils.unregisterEnemyBaseStructure
local registerEnemyBaseStructure = chunkUtils.registerEnemyBaseStructure
local makeImmortalEntity = chunkUtils.makeImmortalEntity
2017-05-19 00:47:24 -07:00
local processBases = baseProcessor.processBases
2016-10-14 17:00:18 -07:00
local mRandom = math.random
2016-08-24 17:05:20 -07:00
-- local references to global
local regionMap -- manages the chunks that make up the game world
local natives -- manages the enemy units, structures, and ai
local pendingChunks -- chunks that have yet to be processed by the mod
2016-08-24 17:05:20 -07:00
-- hook functions
2017-07-04 15:52:20 -07:00
local function onIonCannonFired(event)
--[[
event.force, event.surface, event.player_index, event.position, event.radius
--]]
local surface = event.surface
if (surface.index == 1) then
natives.points = natives.points + 3000
if (natives.points > AI_MAX_OVERFLOW_POINTS) then
natives.points = AI_MAX_OVERFLOW_POINTS
end
local chunk = getChunkByPosition(regionMap, event.position)
2017-11-20 23:27:03 -08:00
if (chunk ~= SENTINEL_IMPASSABLE_CHUNK) then
rallyUnits(chunk, regionMap, surface, natives, event.tick)
2017-07-04 15:52:20 -07:00
end
end
end
local function hookEvents()
if config.ionCannonPresent then
script.on_event(remote.call("orbital_ion_cannon", "on_ion_cannon_fired"),
onIonCannonFired)
-- script.on_event(remote.call("orbital_ion_cannon", "on_ion_cannon_targeted"),
-- onIonCannonTargeted)
end
end
2016-09-12 15:33:00 -07:00
local function onLoad()
2016-08-24 17:05:20 -07:00
regionMap = global.regionMap
natives = global.natives
pendingChunks = global.pendingChunks
2017-07-04 15:52:20 -07:00
hookEvents()
end
2016-09-12 15:33:00 -07:00
local function onChunkGenerated(event)
-- queue generated chunk for delayed processing, queuing is required because some mods (RSO) mess with chunk as they
-- are generated, which messes up the scoring.
if (event.surface.index == 1) then
pendingChunks[#pendingChunks+1] = event
end
2016-08-24 17:05:20 -07:00
end
2017-05-31 18:46:53 -07:00
local function rebuildRegionMap()
game.surfaces[1].print("Rampant - Reindexing chunks, please wait.")
2017-05-27 21:50:37 -07:00
-- clear old regionMap processing Queue
-- prevents queue adding duplicate chunks
-- chunks are by key, so should overwrite old
2017-05-31 18:46:53 -07:00
global.regionMap = {}
regionMap = global.regionMap
2017-05-27 21:50:37 -07:00
regionMap.processQueue = {}
regionMap.processIndex = 1
regionMap.scanIndex = 1
2017-11-20 23:27:03 -08:00
regionMap.chunkToHives = {}
regionMap.chunkToNests = {}
regionMap.chunkToWorms = {}
regionMap.chunkToRetreats = {}
regionMap.chunkToRallys = {}
regionMap.chunkToPlayerBase = {}
regionMap.chunkToResource = {}
regionMap.chunkToPassScan = {}
2017-11-20 23:27:03 -08:00
-- preallocating memory to be used in code, making it fast by reducing garbage generated.
2017-11-20 23:27:03 -08:00
regionMap.neighbors = { SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK }
regionMap.cardinalNeighbors = { SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK,
SENTINEL_IMPASSABLE_CHUNK }
regionMap.position = {x=0,
y=0}
2017-12-20 19:50:36 -08:00
--this is shared between two different queries
regionMap.area = {{0, 0}, {0, 0}}
regionMap.countResourcesQuery = { area=regionMap.area, type="resource" }
regionMap.filteredEntitiesEnemyQuery = { area=regionMap.area, force="enemy" }
regionMap.filteredEntitiesEnemyUnitQuery = { area=regionMap.area, force="enemy", type="unit", limit=301 }
regionMap.filteredEntitiesEnemyTypeQuery = { area=regionMap.area, force="enemy", type="unit-spawner" }
regionMap.filteredEntitiesPlayerQuery = { area=regionMap.area, force="player" }
regionMap.canPlaceQuery = { name="", position={0,0} }
regionMap.filteredTilesQuery = { name="", area=regionMap.area }
2017-05-31 18:46:53 -07:00
-- switched over to tick event
regionMap.logicTick = roundToNearest(game.tick + INTERVAL_LOGIC, INTERVAL_LOGIC)
regionMap.scanTick = roundToNearest(game.tick + INTERVAL_SCAN, INTERVAL_SCAN)
2017-05-31 18:46:53 -07:00
regionMap.processTick = roundToNearest(game.tick + INTERVAL_PROCESS, INTERVAL_PROCESS)
regionMap.chunkTick = roundToNearest(game.tick + INTERVAL_CHUNK, INTERVAL_CHUNK)
regionMap.squadTick = roundToNearest(game.tick + INTERVAL_SQUAD, INTERVAL_SQUAD)
2017-05-31 18:46:53 -07:00
2017-05-27 21:50:37 -07:00
-- clear pending chunks, will be added when loop runs below
2017-06-10 17:59:06 -07:00
global.pendingChunks = {}
pendingChunks = global.pendingChunks
2017-05-27 21:50:37 -07:00
-- queue all current chunks that wont be generated during play
local surface = game.surfaces[1]
2017-05-31 18:46:53 -07:00
local tick = game.tick
2017-05-27 21:50:37 -07:00
for chunk in surface.get_chunks() do
2017-05-31 18:46:53 -07:00
onChunkGenerated({ tick = tick,
2017-05-27 21:50:37 -07:00
surface = surface,
area = { left_top = { x = chunk.x * 32,
y = chunk.y * 32 }}})
2017-05-31 18:46:53 -07:00
end
2017-06-24 11:41:57 -07:00
processPendingChunks(natives, regionMap, surface, pendingChunks, tick)
2017-05-27 21:50:37 -07:00
end
2017-05-06 02:03:28 -07:00
local function onModSettingsChange(event)
2017-05-26 17:58:33 -07:00
2017-05-06 02:03:28 -07:00
if event and (string.sub(event.setting, 1, 7) ~= "rampant") then
2017-05-27 21:50:37 -07:00
return false
2017-05-06 02:03:28 -07:00
end
2017-05-31 18:46:53 -07:00
upgrade.compareTable(natives, "safeBuildings", settings.global["rampant-safeBuildings"].value)
2017-05-06 02:03:28 -07:00
2017-05-31 18:46:53 -07:00
upgrade.compareTable(natives.safeEntities, "curved-rail", settings.global["rampant-safeBuildings-curvedRail"].value)
upgrade.compareTable(natives.safeEntities, "straight-rail", settings.global["rampant-safeBuildings-straightRail"].value)
upgrade.compareTable(natives.safeEntities, "rail-signal", settings.global["rampant-safeBuildings-railSignals"].value)
upgrade.compareTable(natives.safeEntities, "rail-chain-signal", settings.global["rampant-safeBuildings-railChainSignals"].value)
upgrade.compareTable(natives.safeEntities, "train-stop", settings.global["rampant-safeBuildings-trainStops"].value)
2017-06-02 18:24:56 -07:00
upgrade.compareTable(natives.safeEntities, "lamp", settings.global["rampant-safeBuildings-lamps"].value)
2017-05-31 18:46:53 -07:00
local changed, newValue = upgrade.compareTable(natives.safeEntityName,
"big-electric-pole",
settings.global["rampant-safeBuildings-bigElectricPole"].value)
if changed then
natives.safeEntityName["big-electric-pole"] = newValue
natives.safeEntityName["big-electric-pole-2"] = newValue
natives.safeEntityName["big-electric-pole-3"] = newValue
natives.safeEntityName["big-electric-pole-4"] = newValue
end
2017-05-31 18:46:53 -07:00
upgrade.compareTable(natives, "attackUsePlayer", settings.global["rampant-attackWaveGenerationUsePlayerProximity"].value)
upgrade.compareTable(natives, "attackUsePollution", settings.global["rampant-attackWaveGenerationUsePollution"].value)
2017-05-06 02:03:28 -07:00
2017-05-31 18:46:53 -07:00
upgrade.compareTable(natives, "attackThresholdMin", settings.global["rampant-attackWaveGenerationThresholdMin"].value)
upgrade.compareTable(natives, "attackThresholdMax", settings.global["rampant-attackWaveGenerationThresholdMax"].value)
upgrade.compareTable(natives, "attackThresholdRange", natives.attackThresholdMax - natives.attackThresholdMin)
upgrade.compareTable(natives, "attackWaveMaxSize", settings.global["rampant-attackWaveMaxSize"].value)
upgrade.compareTable(natives, "attackPlayerThreshold", settings.global["rampant-attackPlayerThreshold"].value)
upgrade.compareTable(natives, "aiNocturnalMode", settings.global["rampant-permanentNocturnal"].value)
upgrade.compareTable(natives, "aiPointsScaler", settings.global["rampant-aiPointsScaler"].value)
2017-06-10 17:59:06 -07:00
-- RE-ENABLE WHEN COMPLETE
natives.useCustomAI = constants.DEV_CUSTOM_AI
-- changed, newValue = upgrade.compareTable(natives, "useCustomAI", settings.startup["rampant-useCustomAI"].value)
-- if natives.useCustomAI then
-- game.forces.enemy.ai_controllable = false
-- else
-- game.forces.enemy.ai_controllable = true
-- end
-- if changed and newValue then
-- rebuildRegionMap()
-- return false
-- end
2017-05-27 21:50:37 -07:00
return true
end
2017-05-26 17:58:33 -07:00
2017-05-27 21:50:37 -07:00
local function onConfigChanged()
local upgraded
2017-11-20 23:27:03 -08:00
upgraded, natives = upgrade.attempt(natives)
if upgraded and onModSettingsChange(nil) then
2017-05-31 18:46:53 -07:00
rebuildRegionMap()
2017-05-26 17:58:33 -07:00
end
end
2016-11-03 16:51:35 -07:00
local function onTick(event)
local tick = event.tick
2017-01-19 21:58:36 -08:00
if (tick == regionMap.processTick) then
regionMap.processTick = regionMap.processTick + INTERVAL_PROCESS
local gameRef = game
2017-08-10 23:37:23 -07:00
local surface = gameRef.surfaces[1]
processPlayers(gameRef.players, regionMap, surface, natives, tick)
2017-05-26 17:58:33 -07:00
processMap(regionMap, surface, natives, tick)
end
if (tick == regionMap.scanTick) then
regionMap.scanTick = regionMap.scanTick + INTERVAL_SCAN
local surface = game.surfaces[1]
2016-10-14 17:00:18 -07:00
processPendingChunks(natives, regionMap, surface, pendingChunks, tick)
2017-01-19 21:58:36 -08:00
scanMap(regionMap, surface, natives)
regionMap.chunkToPassScan = processScanChunks(regionMap, surface)
end
if (tick == regionMap.logicTick) then
regionMap.logicTick = regionMap.logicTick + INTERVAL_LOGIC
2017-05-31 18:46:53 -07:00
local gameRef = game
local surface = gameRef.surfaces[1]
planning(natives,
gameRef.forces.enemy.evolution_factor,
tick,
surface)
2017-05-19 00:47:24 -07:00
if natives.useCustomAI then
processBases(regionMap, surface, natives, tick)
2017-12-31 11:12:40 -08:00
end
end
if (tick == regionMap.squadTick) then
regionMap.squadTick = regionMap.squadTick + INTERVAL_SQUAD
2016-09-12 15:33:00 -07:00
local gameRef = game
cleanSquads(natives)
regroupSquads(natives)
squadsBeginAttack(natives, gameRef.players)
squadsAttack(regionMap, gameRef.surfaces[1], natives)
2016-08-20 11:24:22 -07:00
end
end
2016-09-12 15:33:00 -07:00
local function onBuild(event)
2017-11-20 23:27:03 -08:00
local entity = event.created_entity
if (entity.surface.index == 1) then
addRemovePlayerEntity(regionMap, entity, natives, true, false)
if natives.safeBuildings then
if natives.safeEntities[entity.type] or natives.safeEntityName[entity.name] then
entity.destructible = false
end
end
end
end
2017-08-08 01:19:51 -07:00
local function onMine(event)
local entity = event.entity
local surface = entity.surface
if (surface.index == 1) then
addRemovePlayerEntity(regionMap, entity, natives, false, false)
end
end
2016-09-12 15:33:00 -07:00
local function onDeath(event)
local entity = event.entity
local surface = entity.surface
if (surface.index == 1) then
2017-11-20 23:27:03 -08:00
local entityPosition = entity.position
local chunk = getChunkByPosition(regionMap, entityPosition)
local cause = event.cause
if (entity.force.name == "enemy") then
if (entity.type == "unit") then
if (chunk ~= SENTINEL_IMPASSABLE_CHUNK) then
2017-01-19 21:58:36 -08:00
-- drop death pheromone where unit died
deathScent(chunk)
2017-01-19 21:58:36 -08:00
if event.force and (event.force.name == "player") and (chunk[MOVEMENT_PHEROMONE] < natives.retreatThreshold) then
local tick = event.tick
2017-05-31 18:46:53 -07:00
local artilleryBlast = (cause and ((cause.type == "artillery-wagon") or (cause.type == "artillery-turret")))
retreatUnits(chunk,
2017-06-08 22:18:59 -07:00
entityPosition,
2017-11-20 23:27:03 -08:00
convertUnitGroupToSquad(natives, entity.unit_group),
regionMap,
2017-05-31 18:46:53 -07:00
surface,
natives,
tick,
(artilleryBlast and RETREAT_SPAWNER_GRAB_RADIUS) or RETREAT_GRAB_RADIUS,
artilleryBlast)
2017-11-20 23:27:03 -08:00
if (mRandom() < natives.rallyThreshold) and not surface.peaceful_mode then
rallyUnits(chunk, regionMap, surface, natives, tick)
2017-01-19 21:58:36 -08:00
end
end
end
elseif event.force and (event.force.name == "player") and (entity.type == "unit-spawner") or (entity.type == "turret") then
local tick = event.tick
if (chunk ~= SENTINEL_IMPASSABLE_CHUNK) then
unregisterEnemyBaseStructure(regionMap, entity)
rallyUnits(chunk, regionMap, surface, natives, tick)
retreatUnits(chunk,
entityPosition,
nil,
regionMap,
surface,
natives,
tick,
RETREAT_SPAWNER_GRAB_RADIUS,
(cause and ((cause.type == "artillery-wagon") or (cause.type == "artillery-turret"))))
end
end
elseif (entity.force.name == "player") then
2016-11-03 16:51:35 -07:00
local creditNatives = false
if (event.force ~= nil) and (event.force.name == "enemy") then
creditNatives = true
if (chunk ~= SENTINEL_IMPASSABLE_CHUNK) then
victoryScent(chunk, entity.type)
2017-05-26 17:58:33 -07:00
end
2016-11-03 16:51:35 -07:00
end
2017-05-06 02:03:28 -07:00
if creditNatives and natives.safeBuildings and (natives.safeEntities[entity.type] or natives.safeEntityName[entity.name]) then
makeImmortalEntity(surface, entity)
else
2017-11-20 23:27:03 -08:00
addRemovePlayerEntity(regionMap, entity, natives, false, creditNatives)
end
end
end
end
2017-05-31 18:46:53 -07:00
local function onEnemyBaseBuild(event)
local entity = event.entity
local surface = entity.surface
if (surface.index == 1) then
registerEnemyBaseStructure(regionMap, entity, nil)
end
2017-05-31 18:46:53 -07:00
end
local function onSurfaceTileChange(event)
local surfaceIndex = event.surface_index or event.robot.surface.index
if (event.item.name == "landfill") and (surfaceIndex == 1) then
local chunks = {}
local positions = event.positions
for i=1,#positions do
local position = positions[i]
local chunk = mapUtils.getChunkByPosition(regionMap, position, true)
-- weird bug with table pointer equality using name instead pointer comparison
if not chunk.name then
regionMap.chunkToPassScan[chunk] = true
else
local x,y = mapUtils.positionToChunkXY(position)
local addMe = true
for ci=1,#chunks do
local c = chunks[ci]
if (c.x == x) and (c.y == y) then
addMe = false
break
end
end
if addMe then
chunks[#chunks+1] = {x=x,y=y}
end
end
end
for i=1,#chunks do
onChunkGenerated({area = { left_top = chunks[i] },
surface = game.surfaces[surfaceIndex]})
end
end
end
local function onResourceDepleted(event)
local entity = event.entity
if (entity.surface.index == 1) then
chunkUtils.unregisterResource(entity, regionMap)
end
end
local function onUsedCapsule(event)
local surface = game.players[event.player_index].surface
if (event.item.name == "cliff-explosives") and (surface.index == 1) then
local cliffs = surface.find_entities_filtered({area={{event.position.x-0.75,event.position.y-0.75},
{event.position.x+0.75,event.position.y+0.75}},
type="cliff"})
for i=1,#cliffs do
2018-01-02 10:36:23 -08:00
entityForPassScan(regionMap, cliffs[i])
end
end
end
2016-09-12 15:33:00 -07:00
local function onInit()
global.regionMap = {}
global.pendingChunks = {}
global.natives = {}
regionMap = global.regionMap
natives = global.natives
pendingChunks = global.pendingChunks
onConfigChanged()
2017-07-04 15:52:20 -07:00
hookEvents()
2016-09-12 15:33:00 -07:00
end
-- hooks
script.on_init(onInit)
script.on_load(onLoad)
2017-11-20 23:27:03 -08:00
script.on_event(defines.events.on_runtime_mod_setting_changed, onModSettingsChange)
2016-08-24 17:05:20 -07:00
script.on_configuration_changed(onConfigChanged)
script.on_event(defines.events.on_resource_depleted, onResourceDepleted)
script.on_event({defines.events.on_player_built_tile,
defines.events.on_robot_built_tile}, onSurfaceTileChange)
script.on_event(defines.events.on_player_used_capsule, onUsedCapsule)
2017-11-20 23:27:03 -08:00
script.on_event(defines.events.on_biter_base_built, onEnemyBaseBuild)
2017-08-13 17:35:44 -07:00
script.on_event({defines.events.on_player_mined_entity,
2017-11-20 23:27:03 -08:00
defines.events.on_robot_mined_entity}, onMine)
script.on_event({defines.events.on_built_entity,
2017-11-20 23:27:03 -08:00
defines.events.on_robot_built_entity}, onBuild)
2016-09-09 03:02:50 -07:00
script.on_event(defines.events.on_entity_died, onDeath)
script.on_event(defines.events.on_tick, onTick)
script.on_event(defines.events.on_chunk_generated, onChunkGenerated)
2017-05-07 23:56:11 -07:00
remote.add_interface("rampantTests",
{
pheromoneLevels = tests.pheromoneLevels,
activeSquads = tests.activeSquads,
entitiesOnPlayerChunk = tests.entitiesOnPlayerChunk,
findNearestPlayerEnemy = tests.findNearestPlayerEnemy,
aiStats = tests.aiStats,
fillableDirtTest = tests.fillableDirtTest,
tunnelTest = tests.tunnelTest,
createEnemy = tests.createEnemy,
attackOrigin = tests.attackOrigin,
cheatMode = tests.cheatMode,
gaussianRandomTest = tests.gaussianRandomTest,
reveal = tests.reveal,
2017-12-31 11:36:23 -08:00
showMovementGrid = tests.showMovementGrid,
2017-05-07 23:56:11 -07:00
baseStats = tests.baseStats,
mergeBases = tests.mergeBases,
2017-05-27 21:50:37 -07:00
clearBases = tests.clearBases,
getOffsetChunk = tests.getOffsetChunk,
2017-06-07 17:57:24 -07:00
registeredNest = tests.registeredNest,
2017-12-31 11:36:23 -08:00
colorResourcePoints = tests.colorResourcePoints,
stepAdvanceTendrils = tests.stepAdvanceTendrils,
exportAiState = tests.exportAiState(onTick)
2017-05-07 23:56:11 -07:00
}
)
2017-05-06 02:03:28 -07:00
remote.add_interface("rampant", interop)