Merge branch 'lua_ai' into 'master'

Control AI packages from Lua

See merge request OpenMW/openmw!1604
C++20
uramer 3 years ago
commit 643c1d6aeb

@ -1,9 +1,17 @@
#include "localscripts.hpp"
#include <components/esm3/loadcell.hpp>
#include "../mwworld/ptr.hpp"
#include "../mwworld/class.hpp"
#include "../mwmechanics/aisequence.hpp"
#include "../mwmechanics/aicombat.hpp"
#include "../mwmechanics/aiescort.hpp"
#include "../mwmechanics/aifollow.hpp"
#include "../mwmechanics/aipursue.hpp"
#include "../mwmechanics/aitravel.hpp"
#include "../mwmechanics/aiwander.hpp"
#include "../mwmechanics/aipackage.hpp"
#include "luamanagerimp.hpp"
@ -60,28 +68,106 @@ namespace MWLua
}
context.mLuaManager->addAction(std::make_unique<SetEquipmentAction>(context.mLua, obj.id(), std::move(eqp)));
};
selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional<LObject>
using AiPackage = MWMechanics::AiPackage;
sol::usertype<AiPackage> aiPackage = context.mLua->sol().new_usertype<AiPackage>("AiPackage");
aiPackage["type"] = sol::readonly_property([](const AiPackage& p) -> std::string_view
{
switch (p.getTypeId())
{
case MWMechanics::AiPackageTypeId::Wander: return "Wander";
case MWMechanics::AiPackageTypeId::Travel: return "Travel";
case MWMechanics::AiPackageTypeId::Escort: return "Escort";
case MWMechanics::AiPackageTypeId::Follow: return "Follow";
case MWMechanics::AiPackageTypeId::Activate: return "Activate";
case MWMechanics::AiPackageTypeId::Combat: return "Combat";
case MWMechanics::AiPackageTypeId::Pursue: return "Pursue";
case MWMechanics::AiPackageTypeId::AvoidDoor: return "AvoidDoor";
case MWMechanics::AiPackageTypeId::Face: return "Face";
case MWMechanics::AiPackageTypeId::Breathe: return "Breathe";
case MWMechanics::AiPackageTypeId::Cast: return "Cast";
default: return "Unknown";
}
});
aiPackage["target"] = sol::readonly_property([worldView=context.mWorldView](const AiPackage& p) -> sol::optional<LObject>
{
MWWorld::Ptr target = p.getTarget();
if (target.isEmpty())
return sol::nullopt;
else
return LObject(getId(target), worldView->getObjectRegistry());
});
aiPackage["sideWithTarget"] = sol::readonly_property([](const AiPackage& p) { return p.sideWithTarget(); });
aiPackage["destination"] = sol::readonly_property([](const AiPackage& p) { return p.getDestination(); });
selfAPI["_getActiveAiPackage"] = [](SelfObject& self) -> sol::optional<std::shared_ptr<AiPackage>>
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
MWWorld::Ptr target;
if (ai.getCombatTarget(target))
return LObject(getId(target), worldView->getObjectRegistry());
if (ai.isEmpty())
return sol::nullopt;
else
return {};
return *ai.begin();
};
selfAPI["stopCombat"] = [](SelfObject& self)
selfAPI["_iterateAndFilterAiSequence"] = [](SelfObject& self, sol::function callback)
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
ai.stopCombat();
std::list<std::shared_ptr<AiPackage>>& list = ai.getUnderlyingList();
for (auto it = list.begin(); it != list.end();)
{
bool keep = LuaUtil::call(callback, *it).get<bool>();
if (keep)
++it;
else
it = list.erase(it);
}
};
selfAPI["startCombat"] = [](SelfObject& self, const LObject& target)
selfAPI["_startAiCombat"] = [](SelfObject& self, const LObject& target)
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
ai.stack(MWMechanics::AiCombat(target.ptr()), ptr);
};
selfAPI["_startAiPursue"] = [](SelfObject& self, const LObject& target)
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
ai.stack(MWMechanics::AiPursue(target.ptr()), ptr);
};
selfAPI["_startAiFollow"] = [](SelfObject& self, const LObject& target)
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
ai.stack(MWMechanics::AiFollow(target.ptr()), ptr);
};
selfAPI["_startAiEscort"] = [](SelfObject& self, const LObject& target, LCell cell,
float duration, const osg::Vec3f& dest)
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
// TODO: change AiEscort implementation to accept ptr instead of a non-unique refId.
const std::string& refId = target.ptr().getCellRef().getRefId();
int gameHoursDuration = static_cast<int>(std::ceil(duration / 3600.0));
const ESM::Cell* esmCell = cell.mStore->getCell();
if (esmCell->isExterior())
ai.stack(MWMechanics::AiEscort(refId, gameHoursDuration, dest.x(), dest.y(), dest.z(), false), ptr);
else
ai.stack(MWMechanics::AiEscort(refId, esmCell->mName, gameHoursDuration, dest.x(), dest.y(), dest.z(), false), ptr);
};
selfAPI["_startAiWander"] = [](SelfObject& self, int distance, float duration)
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
int gameHoursDuration = static_cast<int>(std::ceil(duration / 3600.0));
ai.stack(MWMechanics::AiWander(distance, gameHoursDuration, 0, {}, false), ptr);
};
selfAPI["_startAiTravel"] = [](SelfObject& self, const osg::Vec3f& target)
{
const MWWorld::Ptr& ptr = self.ptr();
MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence();
ai.stack(MWMechanics::AiTravel(target.x(), target.y(), target.z(), false), ptr);
};
}
LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj, ESM::LuaScriptCfg::Flags autoStartMode)

@ -2013,7 +2013,7 @@ namespace MWMechanics
std::list<MWWorld::Ptr> Actors::getActorsFollowing(const MWWorld::Ptr& actor)
{
std::list<MWWorld::Ptr> list;
forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr<AiPackage>& package)
forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::shared_ptr<AiPackage>& package)
{
if (package->followTargetThroughDoors() && package->getTarget() == actor)
list.push_back(iter.first);
@ -2064,7 +2064,7 @@ namespace MWMechanics
std::list<int> Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor)
{
std::list<int> list;
forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr<AiPackage>& package)
forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::shared_ptr<AiPackage>& package)
{
if (package->followTargetThroughDoors() && package->getTarget() == actor)
{
@ -2081,7 +2081,7 @@ namespace MWMechanics
std::map<int, MWWorld::Ptr> Actors::getActorsFollowingByIndex(const MWWorld::Ptr &actor)
{
std::map<int, MWWorld::Ptr> map;
forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr<AiPackage>& package)
forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::shared_ptr<AiPackage>& package)
{
if (package->followTargetThroughDoors() && package->getTarget() == actor)
{

@ -87,17 +87,7 @@ bool AiSequence::getCombatTargets(std::vector<MWWorld::Ptr> &targetActors) const
return !targetActors.empty();
}
std::list<std::unique_ptr<AiPackage>>::const_iterator AiSequence::begin() const
{
return mPackages.begin();
}
std::list<std::unique_ptr<AiPackage>>::const_iterator AiSequence::end() const
{
return mPackages.end();
}
void AiSequence::erase(std::list<std::unique_ptr<AiPackage>>::const_iterator package)
void AiSequence::erase(std::list<std::shared_ptr<AiPackage>>::const_iterator package)
{
// Not sure if manually terminated packages should trigger mDone, probably not?
for(auto it = mPackages.begin(); it != mPackages.end(); ++it)

@ -38,7 +38,7 @@ namespace MWMechanics
class AiSequence
{
///AiPackages to run though
std::list<std::unique_ptr<AiPackage>> mPackages;
std::list<std::shared_ptr<AiPackage>> mPackages;
///Finished with top AIPackage, set for one frame
bool mDone;
@ -63,10 +63,12 @@ namespace MWMechanics
virtual ~AiSequence();
/// Iterator may be invalidated by any function calls other than begin() or end().
std::list<std::unique_ptr<AiPackage>>::const_iterator begin() const;
std::list<std::unique_ptr<AiPackage>>::const_iterator end() const;
std::list<std::shared_ptr<AiPackage>>::const_iterator begin() const { return mPackages.begin(); }
std::list<std::shared_ptr<AiPackage>>::const_iterator end() const { return mPackages.end(); }
void erase(std::list<std::unique_ptr<AiPackage>>::const_iterator package);
void erase(std::list<std::shared_ptr<AiPackage>>::const_iterator package);
std::list<std::shared_ptr<AiPackage>>& getUnderlyingList() { return mPackages; }
/// Returns currently executing AiPackage type
/** \see enum class AiPackageTypeId **/

@ -65,5 +65,6 @@ $DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw/*lua
cd $FILES_DIR/builtin_scripts
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR openmw_aux/*lua
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/ai.lua
$DOCUMENTOR_PATH -f doc -d $OUTPUT_DIR scripts/omw/camera.lua

@ -0,0 +1,145 @@
Built-in AI packages
====================
Combat
------
Attack another actor.
**Arguments**
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - name
- type
- description
* - target
- `GameObject <openmw_core.html##(GameObject)>`_ [required]
- the actor to attack
**Examples**
.. code-block:: Lua
-- from local script add package to self
local AI = require('openmw.interfaces').AI
AI.startPackage({type='Combat', target=anotherActor})
-- via event to any actor
actor:sendEvent('StartAIPackage', {type='Combat', target=anotherActor})
Pursue
------
Pursue another actor.
**Arguments**
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - name
- type
- description
* - target
- `GameObject <openmw_core.html##(GameObject)>`_ [required]
- the actor to pursue
Follow
------
Follow another actor.
**Arguments**
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - name
- type
- description
* - target
- `GameObject <openmw_core.html##(GameObject)>`_ [required]
- the actor to follow
Escort
------
Escort another actor to the given location.
**Arguments**
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - name
- type
- description
* - target
- `GameObject <openmw_core.html##(GameObject)>`_ [required]
- the actor to follow
* - destPosition
- `3d vector <openmw_util.html##(Vector3)>`_ [required]
- the destination point
* - destCell
- Cell [optional]
- the destination cell
* - duration
- number [optional]
- duration in game time (will be rounded up to the next hour)
**Example**
.. code-block:: Lua
actor:sendEvent('StartAIPackage', {
type = 'Escort',
target = object.self,
destPosition = util.vector3(x, y, z),
duration = 3 * time.hour,
})
Wander
------
Wander nearby current position.
**Arguments**
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - name
- type
- description
* - distance
- float [default=0]
- the actor to follow
* - duration
- number [optional]
- duration in game time (will be rounded up to the next hour)
Travel
------
Go to given location.
**Arguments**
.. list-table::
:header-rows: 1
:widths: 20 20 60
* - name
- type
- description
* - destPosition
- `3d vector <openmw_util.html##(Vector3)>`_ [required]
- the point to travel to

@ -7,6 +7,8 @@ Lua API reference
engine_handlers
user_interface
aipackages
events
openmw_util
openmw_storage
openmw_core
@ -21,6 +23,7 @@ Lua API reference
openmw_aux_calendar
openmw_aux_util
openmw_aux_time
interface_ai
interface_camera
@ -28,6 +31,8 @@ Lua API reference
- :ref:`User interface reference <User interface reference>`
- `Game object reference <openmw_core.html##(GameObject)>`_
- `Cell reference <openmw_core.html##(Cell)>`_
- :ref:`Built-in AI packages`
- :ref:`Built-in events`
**API packages**
@ -87,6 +92,8 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Interface | Can be used | Description |
+=========================================================+====================+===============================================================+
|:ref:`AI <Interface AI>` | by local scripts | | Control basic AI of NPCs and creatures. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
| | | | without overriding the script completely. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+

@ -0,0 +1,13 @@
Built-in events
===============
Any script can send to any actor (except player, for player will be ignored) events ``StartAIPackage`` and ``RemoveAIPackages``.
The effect is equivalent to calling ``interfaces.AI.startPackage`` or ``interfaces.AI.removePackages`` in a local script on this actor.
Examples:
.. code-block:: Lua
actor:sendEvent('StartAIPackage', {type='Combat', target=self.object})
actor:sendEvent('RemoveAIPackages', 'Pursue')

@ -0,0 +1,6 @@
Interface AI
============
.. raw:: html
:file: generated_html/scripts_omw_ai.html

@ -463,6 +463,8 @@ The order in which the scripts are started is important. So if one mod should ov
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
| Interface | Can be used | Description |
+=========================================================+====================+===============================================================+
|:ref:`AI <Interface AI>` | by local scripts | | Control basic AI of NPCs and creatures. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`Camera <Interface Camera>` | by player scripts | | Allows to alter behavior of the built-in camera script |
| | | | without overriding the script completely. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
@ -495,7 +497,7 @@ At some moment it will send the 'DamagedByDarkPower' event to all nearby actors:
local self = require('openmw.self')
local nearby = require('openmw.nearby')
local function onActivate()
local function onActivated()
for i, actor in nearby.actors:ipairs() do
local dist = (self.position - actor.position):length()
if dist < 500 then
@ -505,7 +507,7 @@ At some moment it will send the 'DamagedByDarkPower' event to all nearby actors:
end
end
return { engineHandlers = { ... } }
return { engineHandlers = { onActivated = onActivated } }
And every actor should have a local script that processes this event:
@ -537,6 +539,8 @@ The protection mod attaches an additional local script to every actor. The scrip
In order to be able to intercept the event, the protection script should be placed in the load order below the original script.
See :ref:`the list of events <Built-in events>` that are used by built-in scripts.
Timers
======

@ -1 +1,2 @@
PLAYER: scripts/omw/camera.lua
NPC,CREATURE: scripts/omw/ai.lua

@ -0,0 +1,116 @@
local self = require('openmw.self')
local interfaces = require('openmw.interfaces')
local function startPackage(args)
if args.type == 'Combat' then
if not args.target then error("target required") end
self:_startAiCombat(args.target)
elseif args.type == 'Pursue' then
if not args.target then error("target required") end
self:_startAiPursue(args.target)
elseif args.type == 'Follow' then
if not args.target then error("target required") end
self:_startAiFollow(args.target)
elseif args.type == 'Escort' then
if not args.target then error("target required") end
if not args.destPosition then error("destPosition required") end
self:_startAiEscort(args.target, args.destCell or self.cell, args.duration or 0, args.destPosition)
elseif args.type == 'Wander' then
self:_startAiWander(args.distance or 0, args.duration or 0)
elseif args.type == 'Travel' then
if not args.destPosition then error("destPosition required") end
self:_startAiTravel(args.destPosition)
else
error('Unsupported AI Package: '..args.type)
end
end
local function filterPackages(filter)
self:_iterateAndFilterAiSequence(filter)
end
return {
interfaceName = 'AI',
--- Basic AI interface
-- @module AI
-- @usage require('openmw.interfaces').AI
interface = {
--- Interface version
-- @field [parent=#AI] #number version
version = 0,
--- AI Package
-- @type Package
-- @field #string type Type of the AI package.
-- @field openmw.core#GameObject target Target (usually an actor) of the AI package (can be nil).
-- @field #boolean sideWithTarget Whether to help the target in combat (true or false).
-- @field openmw.util#Vector3 position Destination point of the AI package (can be nil).
--- Return the currently active AI package (or `nil` if there are no AI packages).
-- @function [parent=#AI] getActivePackage
-- @return #Package
getActivePackage = function() return self:_getActiveAiPackage() end,
--- Start new AI package.
-- @function [parent=#AI] startPackage
-- @param #table options See the "Built-in AI packages" page.
startPackage = startPackage,
--- Iterate over all packages starting from the active one and remove those where `filterCallback` returns false.
-- @function [parent=#AI] filterPackages
-- @param #function filterCallback
filterPackages = filterPackages,
--- Iterate over all packages and run `callback` for each starting from the active one.
-- The same as `filterPackage`, but without removal.
-- @function [parent=#AI] forEachPackage
-- @param #function callback
forEachPackage = function(callback)
local filter = function(p)
callback(p)
return true
end
filterPackages(filter)
end,
--- Remove packages of given type (remove all packages if the type is not specified).
-- @function [parent=#AI] removePackages
-- @param #string packageType (optional) The type of packages to remove.
removePackages = function(packageType)
filterPackages(function(p) return packageType and p.type ~= packageType end)
end,
--- Return the target of the active package if the package has given type
-- @function [parent=#AI] getActiveTarget
-- @param #string packageType The expected type of the active package
-- @return openmw.core#GameObject The target (can be nil if the package has no target or has another type)
getActiveTarget = function(packageType)
local p = self:_getActiveAiPackage()
if p and p.type == packageType then
return p.target
else
return nil
end
end,
--- Get list of targets of all packages of the given type.
-- @function [parent=#AI] getTargets
-- @param #string packageType
-- @return #list<openmw.core#GameObject>
getTargets = function(packageType)
local res = {}
filterPackages(function(p)
if p.type == packageType and p.target then
res[#res + 1] = p.target
end
return true
end)
return res
end,
},
eventHandlers = {
StartAIPackage = function(options) interfaces.AI.startPackage(options) end,
RemoveAIPackages = function(packageType) interfaces.AI.removePackages(packageType) end,
},
}

@ -37,27 +37,10 @@
-- @field [parent=#ActorControls] #number use if 1 - activates the readied weapon/spell. For weapons, keeping at 1 will charge the attack until set to 0.
-------------------------------------------------------------------------------
-- Enables or disables standart AI (enabled by default).
-- Enables or disables standard AI (enabled by default).
-- @function [parent=#self] enableAI
-- @param self
-- @param #boolean v
-------------------------------------------------------------------------------
-- Returns current target or nil if not in combat
-- @function [parent=#self] getCombatTarget
-- @param self
-- @return openmw.core#GameObject
-------------------------------------------------------------------------------
-- Remove all combat packages from the actor.
-- @function [parent=#self] stopCombat
-- @param self
-------------------------------------------------------------------------------
-- Attack `target`.
-- @function [parent=#self] startCombat
-- @param self
-- @param openmw.core#GameObject target
return nil

Loading…
Cancel
Save