From bb2a77c4f8c1b1d6e15ceb8f274331a949e19443 Mon Sep 17 00:00:00 2001 From: Ragora Date: Thu, 20 Nov 2014 00:12:25 -0500 Subject: [PATCH] Added source --- scripts/DXAI_Config.cs | 9 + scripts/DXAI_Helpers.cs | 167 +++++++++++++++++++ scripts/DXAI_Main.cs | 325 +++++++++++++++++++++++++++++++++++++ scripts/DXAI_Objectives.cs | 75 +++++++++ 4 files changed, 576 insertions(+) create mode 100644 scripts/DXAI_Config.cs create mode 100644 scripts/DXAI_Helpers.cs create mode 100644 scripts/DXAI_Main.cs create mode 100644 scripts/DXAI_Objectives.cs diff --git a/scripts/DXAI_Config.cs b/scripts/DXAI_Config.cs new file mode 100644 index 0000000..bf25761 --- /dev/null +++ b/scripts/DXAI_Config.cs @@ -0,0 +1,9 @@ +// DXAI_Config.cs +// Configuration for the DXAI System +// Copyright (c) 2014 Robert MacGregor + +$DXAI::Commander::minimumFlagDefense = 1; +$DXAI::Commander::minimumGeneratorDefense = 1; + +$DXAI::Bot::DefaultFieldOfView = 3.14159 / 2; // 90* +$DXAI::Bot::DefaultViewDistance = 300; diff --git a/scripts/DXAI_Helpers.cs b/scripts/DXAI_Helpers.cs new file mode 100644 index 0000000..d3acde4 --- /dev/null +++ b/scripts/DXAI_Helpers.cs @@ -0,0 +1,167 @@ +// DXAI_Helpers.cs +// Helper functions for the AI System +// Copyright (c) 2014 Robert MacGregor + +function sameSide(%p1, %p2, %a, %b) +{ + %cp1 = vectorCross(vectorSub(%b, %a), vectorSub(%p1, %a)); + %cp2 = vectorCross(vectorSub(%b, %a), vectorSub(%p2, %a)); + + if (vectorDot(%cp1, %cp2) >= 0) + return true; + + return false; +} + +function pointInTriangle(%point, %a, %b, %c) +{ + if (sameSide(%point, %a, %b, %c) && sameSide(%point, %b, %a, %c) && sameSide(%point, %c, %a, %b)) + return true; + + return false; +} + +function AIConnection::reset(%this) +{ + AIUnassignClient(%this); + + %this.stop(); + %this.clearTasks(); + %this.clearStep(); + %this.lastDamageClient = -1; + %this.lastDamageTurret = -1; + %this.shouldEngage = -1; + %this.setEngageTarget(-1); + %this.setTargetObject(-1); + %this.pilotVehicle = false; + %this.defaultTasksAdded = false; + + if (isObject(%this.controlByHuman)) + aiReleaseHumanControl(%this.controlByHuman, %this); +} + +// TODO: Return in a faster-to-read format: Could try as static GVar names +// as the game's scripting environment for the gameplay is single threaded +// and it probably does a hash to store the values. +// TODO: Mathematical optimizations, right now it's a hack because of no +// reliable way of getting a player's X facing? +function GameConnection::calculateViewCone(%this, %distance) +{ + //%xFacing = %this.player.getXFacing(); + %halfView = %this.fieldOfView / 2; + %coneOrigin = %this.player.getMuzzlePoint($WeaponSlot); + + %forwardVector = %this.player.getForwardVector(); + %sideVector = vectorCross("0 0 1", %forwardVector); + + // Clockwise + //%viewConeClockwise = %xFacing - %halfView; + + // %viewConeClockwisePoint = mCos(%viewConeClockwise) SPC mSin(%viewConeClockwise) SPC "0"; + %viewConeClockwisePoint = mCos(-%halfView) SPC mSin(-%halfView) SPC "0"; + %viewConeClockwisePoint = vectorScale(%viewConeClockwisePoint, %this.viewDistance); + //%viewConeClockwisePoint = vectorAdd(%viewConeClockwisePoint, %coneOrigin); + + // Counter Clockwise + //%viewConeCounterClockwise = %xFacing + %halfView; + + //%viewConeCounterClockwisePoint = mCos(%viewConeCounterClockwise) SPC mSin(%viewConeCounterClockwise) SPC "0"; + %viewConeCounterClockwisePoint = mCos(%halfView) SPC mSin(%halfView) SPC "0"; + %viewConeCounterClockwisePoint = vectorScale(%viewConeCounterClockwisePoint, %this.viewDistance); + //%viewConeCounterClockwisePoint = vectorAdd(%viewConeCounterClockwisePoint, %coneOrigin); + + // Offsets + %halfDistance = vectorDist(%viewConeCounterClockwisePoint, %viewConeClockwisePoint) / 2; + + %viewConeCounterClockwisePoint = vectorScale(%sideVector, %halfDistance); + %viewConeCounterClockwisePoint = vectorAdd(%coneOrigin, %viewConeCounterClockwisePoint); + + %viewConeClockwisePoint = vectorScale(vectorScale(%sideVector, -1), %halfDistance); + %viewConeClockwisePoint = vectorAdd(%coneOrigin, %viewConeClockwisePoint); + + // Translate the upper and lower points + %viewForwardPoint = vectorScale(%forwardVector, %this.viewDistance); + + %viewConeUpperPoint = vectorAdd(vectorScale("0 0 1", %halfDistance), %viewForwardPoint); + %viewConeUpperPoint = vectorAdd(%coneOrigin, %viewConeUpperPoint); + + %viewConeLowerPoint = vectorAdd(vectorScale("0 0 -1", %halfDistance), %viewForwardPoint); + %viewConeLowerPoint = vectorAdd(%coneOrigin, %viewConeLowerPoint); + + // Now cast them forward + %viewConeClockwisePoint = vectorAdd(%viewConeClockwisePoint, vectorScale(%this.player.getForwardVector(), %this.viewDistance)); + %viewConeCounterClockwisePoint = vectorAdd(%viewConeCounterClockwisePoint, vectorScale(%this.player.getForwardVector(), %this.viewDistance)); + + return %coneOrigin SPC %viewConeClockwisePoint SPC %viewConeCounterClockwisePoint SPC %viewConeUpperPoint SPC %viewConeLowerPoint; +} + +// View cone simulation function +function GameConnection::getObjectsInViewcone(%this, %typeMask, %distance, %performLOSTest) +{ + // FIXME: Radians + if (%this.fieldOfView < 0 || %this.fieldOfView > 3.14) + { + %this.fieldOfView = $DXAPI::Bot::DefaultFieldOfView; + error("DXAI: Bad field of view value! (" @ %this @ ".fieldOfView > 3.14 || " @ %this @ ".fieldOfView < 0)"); + } + + if (%this.viewDistance <= 0) + { + %this.viewDistance = $DXAPI::Bot::DefaultViewDistance; + error("DXAI: Bad view distance value! (" @ %this @ ".viewDistance <= 0)"); + } + + if (%distance $= "") + %distance = %this.viewDistance; + + %viewCone = %this.calculateViewCone(%distance); + + // Extract the results: See TODO above ::calculateViewCone implementation + %coneOrigin = getWords(%viewCone, 0, 2); + %viewConeClockwiseVector = getWords(%viewCone, 3, 5); + %viewConeCounterClockwiseVector = getWords(%viewCone, 6, 8); + %viewConeUpperVector = getWords(%viewCone, 9, 11); + %viewConeLowerVector = getWords(%viewCone, 12, 14); + + %result = new SimSet(); + + // Doing a radius search should hopefully be faster than iterating over all objects in MissionCleanup. + // Even if the game did that internally it's definitely faster than doing it in TS + InitContainerRadiusSearch(%coneOrigin, %distance, %typeMask); + while((%currentObject = containerSearchNext()) != 0) + { + if (%currentObject == %this || !isObject(%currentObject) || containerSearchCurrRadDamageDist() > %distance) + continue; + + // Check if the object is within both the horizontal and vertical triangles representing our view cone + if (%currentObject.getType() & %typeMask && pointInTriangle(%currentObject.getPosition(), %viewConeClockwiseVector, %viewConeCounterClockwiseVector, %coneOrigin) && pointInTriangle(%currentObject.getPosition(), %viewConeLowerVector, %viewConeUpperVector, %coneOrigin)) + { + if (!%performLOSTest) + %result.add(%currentObject); + else + { + %rayCast = containerRayCast(%coneOrigin, %currentObject.getWorldBoxCenter(), -1, 0); + + %hitObject = getWord(%raycast, 0); + + // Since the engine doesn't do raycasts against projectiles correctly, we just check if the bot + // hit -nothing- when doing the raycast rather than checking for a hit against the object + if (%hitObject == %currentObject || (%currentObject.getType() & $TypeMasks::ProjectileObjectType && !isObject(%hitObject))) + %result.add(%currentObject); + } + } + } + + return %result; +} + +// If the map editor was instantiated, this will prevent a little bit +// of console warnings +function Terraformer::getType(%this) { return 0; } + +// Dummy ScriptObject methods to silence console warnings when testing the runtime +// environment; this may not silence for all mods but it should help. +$DXAI::System::RuntimeDummy = new ScriptObject(RuntimeDummy) { class = "RuntimeDummy"; }; + +function RuntimeDummy::addTask() { } +function RuntimeDummy::reset() { } diff --git a/scripts/DXAI_Main.cs b/scripts/DXAI_Main.cs new file mode 100644 index 0000000..b85d890 --- /dev/null +++ b/scripts/DXAI_Main.cs @@ -0,0 +1,325 @@ +// DXAI_Main.cs +// Experimental AI System for ProjectR3 +// Copyright (c) 2014 Robert MacGregor + +exec("scripts/Server/DXAI_Objectives.cs"); +exec("scripts/Server/DXAI_Helpers.cs"); +exec("scripts/Server/DXAI_Config.cs"); + +$DXAI::ActiveCommanderCount = 2; + +// AICommander +// This is a script object that exists for every team in a given +// gamemode and performs the coordination of bots in the game. + +function AICommander::notifyPlayerDeath(%this, %killed, %killedBy) +{ +} + +function AICommander::setup(%this) +{ + %this.botList = new Simset(); + %this.idleBotList = new Simset(); + + for (%iteration = 0; %iteration < ClientGroup.getCount(); %iteration++) + { + %currentClient = ClientGroup.getObject(%iteration); + + if (%currentClient.team == %this.team && %currentClient.isAIControlled()) + { + %this.botList.add(%currentClient); + %this.idleBotList.add(%currentClient); + + %currentClient.commander = %this; + } + } +} + +function AICommander::removeBot(%this, %bot) +{ + %this.botList.remove(%bot); + %this.idleBotList.remove(%bot); + + %bot.commander = -1; +} + +function AICommander::addBot(%this, %bot) +{ + if (!%this.botList.isMember(%bot)) + %this.botList.add(%bot); + + if (!%this.idleBotList.isMember(%bot)) + %this.idleBotList.add(%bot); + + %bot.commander = %this; +} + +function AICommander::cleanup(%this) +{ + %this.botList.delete(); + %this.idleBotList.delete(); +} + +function AICommander::update(%this) +{ + for (%iteration = 0; %iteration < %this.botList.getCount(); %iteration++) + %this.botList.getObject(%iteration).update(); +} + +// General DXAI API implementations +function DXAI::cleanup() +{ + $DXAI::System::Setup = false; + + for (%iteration = 1; %iteration < $DXAI::ActiveCommanderCount + 1; %iteration++) + { + $DXAI::ActiveCommander[%iteration].cleanup(); + $DXAI::ActiveCommander[%iteration].delete(); + } + + $DXAI::ActiveCommanderCount = 0; +} + +function DXAI::setup(%numTeams) +{ + // Mark the environment as invalidated for each new run so that our hooks + // can be verified + $DXAI::System::InvalidatedEnvironment = true; + + // Set our setup flag so that the execution hooks can behave correctly + $DXAI::System::Setup = true; + + for (%iteration = 1; %iteration < %numTeams + 1; %iteration++) + { + %commander = new ScriptObject() { class = "AICommander"; team = %iteration; }; + %commander.setup(); + + $DXAI::ActiveCommander[%iteration] = %commander; + } + + // And setup the default values + for (%iteration = 0; %iteration < ClientGroup.getCount(); %iteration++) + { + %currentClient = ClientGroup.getObject(%iteration); + + %currentClient.viewDistance = $DXAI::Bot::DefaultViewDistance; + %currentClient.fieldOfView = $DXAI::Bot::DefaultFieldOfView; + } + + $DXAI::ActiveCommanderCount = %numTeams; +} + +function DXAI::validateEnvironment() +{ + %gameModeName = $CurrentMissionType @ "Game"; + + %payloadTemplate = %payload = "function " @ %gameModeName @ "::() { return DefaultGame::($DXAI::System::RuntimeDummy); } "; + if (game.AIChooseGameObjective($DXAI::System::RuntimeDummy) != 11595) + { + error("DXAI: Function 'DefaultGame::AIChooseGameObjective' detected to be overwritten by the current gamemode. Correcting ..."); + + eval(%strReplace(%payloadTemplate, "", "AIChooseGameObjective")); + + // Make sure the patch took + if (game.AIChooseGameObjective($DXAI::System::RuntimeDummy) != 11595) + error("DXAI: Failed to patch 'DefaultGame::AIChooseGameObjective'! DXAI may not function correctly."); + } + + if (game.onAIRespawn($DXAI::System::RuntimeDummy) != 11595) + { + error("DXAI: Function 'DefaultGame::onAIRespawn' detected to be overwritten by the current gamemode. Correcting ... "); + + eval(%strReplace(%payloadTemplate, "", "onAIRespawn")); + + if (game.onAIRespawn($DXAI::System::RuntimeDummy) != 11595) + error("DXAI: Failed to patch 'DefaultGame::onAIRespawn'! DXAI may not function correctly."); + } + + $DXAI::System::InvalidatedEnvironment = false; +} + +function DXAI::update() +{ + if (isEventPending($DXAI::updateHandle)) + cancel($DXAI::updateHandle); + + // Check if the bound functions are overwritten by the current gamemode, or if something + // may have invalidated our hooks + if ($DXAI::System::InvalidatedEnvironment && $DXAI::System::Setup) + DXAI::validateEnvironment(); + + for (%iteration = 1; %iteration < $DXAI::ActiveCommanderCount + 1; %iteration++) + $DXAI::ActiveCommander[%iteration].update(); + + // Apparently we can't schedule a bound function otherwise + $DXAI::updateHandle = schedule(32,0,"eval", "DXAI::update();"); +} + +function DXAI::notifyPlayerDeath(%killed, %killedBy) +{ + for (%iteration = 1; %iteration < $DXAI::ActiveCommanderCount + 1; %iteration++) + $DXAI::ActiveCommander[%iteration].notifyPlayerDeath(%killed, %killedBy); +} + +// AIPlayer +// This is a script object that contains DXAI functionality on a per-soldier +// basis +function AIConnection::initialize(%this, %aiClient) +{ + %this.fieldOfView = 3.14 / 2; // 90* View cone + %this.viewDistance = 300; + + if (!isObject(%aiClient)) + error("AIPlayer: Attempted to initialize with bad AI client connection!"); + + %this.client = %aiClient; +} + +function AIConnection::update(%this) +{ +} + +function AIConnection::notifyProjectileImpact(%this, %data, %proj, %position) +{ + if (!isObject(%proj.sourceObject) || %proj.sourceObject.client.team == %this.team) + return; +} + +function AIConnection::isIdle(%this) +{ + if (!isObject(%this.commander)) + return true; + + return %this.commander.idleBotList.isMember(%this); +} + + +// Hooks for the AI System +package DXAI_Hooks +{ + function DefaultGame::gameOver(%game) + { + parent::gameOver(%game); + + DXAI::cleanup(); + } + + function DefaultGame::startMatch(%game) + { + parent::startMatch(%game); + + DXAI::setup(%game.numTeams); + DXAI::update(); + } + + function DefaultGame::AIChangeTeam(%game, %client, %newTeam) + { + // Remove us from the old commander's control first + $DXAI::ActiveCommander[%client.team].removeBot(%client); + + parent::AIChangeTeam(%game, %client, %newTeam); + + $DXAI::ActiveCommander[%newTeam].addBot(%client); + } + + function AIConnection::onAIDrop(%client) + { + if (isObject(%client.commander)) + %client.commander.removeBot(%client); + + parent::onAIDrop(%client); + } + + // Hooks for AI System notification + function DefaultGame::onClientKilled(%game, %clVictim, %clKiller, %damageType, %implement, %damageLocation) + { + parent::onClientKilled(%game, %clVictim, %clKiller, %damageType, %implement, %damageLocation); + + DXAI::notifyPlayerDeath(%clVictim, %clKiller); + } + + function DefaultGame::onAIKilled(%game, %clVictim, %clKiller, %damageType, %implement) + { + parent::onAIKilled(%game, %clVictim, %clKiller, %damageType, %implement); + + DXAI::notifyPlayerDeath(%clVictim, %clKiller); + } + + function ProjectileData::onExplode(%data, %proj, %pos, %mod) + { + parent::onExplode(%data, %proj, %pos, %mod); + + // Look for any bots nearby + InitContainerRadiusSearch(%pos, 10, $TypeMasks::PlayerObjectType); + + while ((%targetObject = containerSearchNext()) != 0) + { + %currentDistance = containerSearchCurrRadDamageDist(); + + if (%currentDistance > 10 || !%targetObject.client.isAIControlled()) + continue; + + // Get the projectile team + %projectileTeam = -1; + if (isObject(%proj.sourceObject)) + %projectileTeam = %proj.sourceObject.client.team; + + // Determine if we should run based on team & Team damage + %shouldRun = false; + if (isObject(%proj.sourceObject) && %projectileTeam == %targetObject.client.team && $TeamDamage) + %shouldRun = true; + else if (isObject(%proj.sourceObject) && %projectileTeam != %targetObject.client.team) + %shouldRun = true; + + // Determine if we 'heard' it. The sound distance seems to be roughly 55m or less and we check the maxDistance + // IIRC The 55m distance should scale with the min/max distances and volume but I'm not sure how those interact + %heardHit = false; + %hitDistance = vectorDist(%targetObject.getWorldBoxCenter(), %pos); + + if (%hitDistance < 55 && %hitDistance <= %data.explosion.soundProfile.description.maxDistance) + %heardHit = true; + + // If the thing has any radius damage (and we heard it), run around a little bit if we need to + if (%data.indirectDamage != 0 && %shouldRun) + %targetObject.client.setDangerLocation(%pos, 20); + + // If we should care and it wasn't a teammate projectile, notify + if (%shouldRun && %projectileTeam != %targetObject.client.team) + %targetObject.client.notifyProjectileImpact(%data, %proj, %pos); + } + } + + // Make this do nothing so the bots don't ever get any objectives by default + function DefaultGame::AIChooseGameObjective(%game, %client) { return 11595; } + + function DefaultGame::onAIRespawn(%game, %client) + { + // Make sure the bot has no objectives + %client.reset(); + %client.defaultTasksAdded = true; + + // All bots have this task, see DXAI_Objectives.cs + %client.addTask("AIVisualAcuity"); + + return 11595; + } + + // We package hook the exec() and compile() functions to perform execution environment + // checking because these can easily load code that overwrites methods that are otherwise + // hooked by DXAI. This can happen with gamemode specific events because DXAI only hooks into + // DefaultGame. This is mostly helpful for developers. + function exec(%file) + { + $DXAI::System::InvalidatedEnvironment = true; + parent::exec(%file); + } + + function compile(%file) + { + $DXAI::System::InvalidatedEnvironment = true; + parent::compile(%file); + } +}; + +if (!isActivePackage(DXAI_Hooks)) + activatePackage(DXAI_Hooks); diff --git a/scripts/DXAI_Objectives.cs b/scripts/DXAI_Objectives.cs new file mode 100644 index 0000000..2591f68 --- /dev/null +++ b/scripts/DXAI_Objectives.cs @@ -0,0 +1,75 @@ +// DXAI_Objectives.cs +// Objectives for the AI system +// Copyright (c) 2014 Robert MacGregor + +//---------------------------------------------------------------------- +// The AIVisualAcuity task is a complementary task for the AI grunt systems +// to perform better at recognizing things visually with reasonably +// Human perception capabilities. +// --------------------------------------------------------------------- + +function AIVisualAcuity::initFromObjective(%task, %objective, %client) +{ + // Called to initialize from an objective object +} + +function AIVisualAcuity::assume(%task, %client) +{ + // Called when the bot starts the task +} + +function AIVisualAcuity::retire(%task, %client) +{ + // Called when the bot stops the task +} + +function AIVisualAcuity::weight(%task, %client) +{ + %task.setWeight(999); +} + +function AIVisualAcuity::monitor(%task, %client) +{ + // Called when the bot is performing the task + + if (%client.enableVisualDebug) + { + if (!isObject(%client.originMarker)) + { + %client.originMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %client.team; name = %client.namebase SPC " Origin"; }; + %client.clockwiseMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %client.team; name = %client.namebase SPC " Clockwise"; }; + %client.counterClockwiseMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %client.team; name = %client.namebase SPC " Counter Clockwise"; }; + %client.upperMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %client.team; name = %client.namebase SPC " Upper"; }; + %client.lowerMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %client.team; name = %client.namebase SPC " Lower"; }; + } + + %viewCone = %client.calculateViewCone(); + %coneOrigin = getWords(%viewCone, 0, 2); + %viewConeClockwiseVector = getWords(%viewCone, 3, 5); + %viewConeCounterClockwiseVector = getWords(%viewCone, 6, 8); + + %viewConeUpperVector = getWords(%viewCone, 9, 11); + %viewConeLowerVector = getWords(%viewCone, 12, 14); + + // Update all the markers + %client.clockwiseMarker.setPosition(%viewConeClockwiseVector); + %client.counterClockwiseMarker.setPosition(%viewConeCounterClockwiseVector); + %client.upperMarker.setPosition(%viewConeUpperVector); + %client.lowerMarker.setPosition(%viewConeLowerVector); + %client.originMarker.setPosition(%coneOrigin); + } + else if (isObject(%client.originMarker)) + { + %client.originMarker.delete(); + %client.clockwiseMarker.delete(); + %client.counterClockwiseMarker.delete(); + %client.upperMarker.delete(); + %client.lowerMarker.delete(); + } + + %result = %client.getObjectsInViewcone($TypeMasks::ProjectileObjectType | $TypeMasks::PlayerObjectType, %client.viewDistance, true); + + echo(%result.getCount()); + + %result.delete(); +}