diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 2b492c5c17..552ce77541 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -1,9 +1,17 @@ #include "localscripts.hpp" +#include + #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(context.mLua, obj.id(), std::move(eqp))); }; - selfAPI["getCombatTarget"] = [worldView=context.mWorldView](SelfObject& self) -> sol::optional + + using AiPackage = MWMechanics::AiPackage; + sol::usertype aiPackage = context.mLua->sol().new_usertype("AiPackage"); + aiPackage["type"] = sol::readonly_property([](const AiPackage& p) -> std::string_view { - 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()); + 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 + { + MWWorld::Ptr target = p.getTarget(); + if (target.isEmpty()) + return sol::nullopt; else - return {}; - }; - selfAPI["stopCombat"] = [](SelfObject& self) + 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> { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - ai.stopCombat(); + if (ai.isEmpty()) + return sol::nullopt; + else + return *ai.begin(); }; - selfAPI["startCombat"] = [](SelfObject& self, const LObject& target) + selfAPI["_iterateAndFilterAiSequence"] = [](SelfObject& self, sol::function callback) + { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + std::list>& list = ai.getUnderlyingList(); + for (auto it = list.begin(); it != list.end();) + { + bool keep = LuaUtil::call(callback, *it).get(); + if (keep) + ++it; + else + it = list.erase(it); + } + }; + 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(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(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) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index c567ea464c..2f447e12d6 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -2013,7 +2013,7 @@ namespace MWMechanics std::list Actors::getActorsFollowing(const MWWorld::Ptr& actor) { std::list list; - forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::shared_ptr& package) { if (package->followTargetThroughDoors() && package->getTarget() == actor) list.push_back(iter.first); @@ -2064,7 +2064,7 @@ namespace MWMechanics std::list Actors::getActorsFollowingIndices(const MWWorld::Ptr &actor) { std::list list; - forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::shared_ptr& package) { if (package->followTargetThroughDoors() && package->getTarget() == actor) { @@ -2081,7 +2081,7 @@ namespace MWMechanics std::map Actors::getActorsFollowingByIndex(const MWWorld::Ptr &actor) { std::map map; - forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::unique_ptr& package) + forEachFollowingPackage(mActors, actor, getPlayer(), [&] (auto& iter, const std::shared_ptr& package) { if (package->followTargetThroughDoors() && package->getTarget() == actor) { diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index b9efcb1cde..86bc714964 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -87,17 +87,7 @@ bool AiSequence::getCombatTargets(std::vector &targetActors) const return !targetActors.empty(); } -std::list>::const_iterator AiSequence::begin() const -{ - return mPackages.begin(); -} - -std::list>::const_iterator AiSequence::end() const -{ - return mPackages.end(); -} - -void AiSequence::erase(std::list>::const_iterator package) +void AiSequence::erase(std::list>::const_iterator package) { // Not sure if manually terminated packages should trigger mDone, probably not? for(auto it = mPackages.begin(); it != mPackages.end(); ++it) diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 853509ed80..6cbfcf045d 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -38,7 +38,7 @@ namespace MWMechanics class AiSequence { ///AiPackages to run though - std::list> mPackages; + std::list> 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>::const_iterator begin() const; - std::list>::const_iterator end() const; + std::list>::const_iterator begin() const { return mPackages.begin(); } + std::list>::const_iterator end() const { return mPackages.end(); } - void erase(std::list>::const_iterator package); + void erase(std::list>::const_iterator package); + + std::list>& getUnderlyingList() { return mPackages; } /// Returns currently executing AiPackage type /** \see enum class AiPackageTypeId **/ diff --git a/docs/source/generate_luadoc.sh b/docs/source/generate_luadoc.sh index 067a1ad4cf..99e387cf0f 100755 --- a/docs/source/generate_luadoc.sh +++ b/docs/source/generate_luadoc.sh @@ -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 diff --git a/docs/source/reference/lua-scripting/aipackages.rst b/docs/source/reference/lua-scripting/aipackages.rst new file mode 100644 index 0000000000..4570be712e --- /dev/null +++ b/docs/source/reference/lua-scripting/aipackages.rst @@ -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 `_ [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 `_ [required] + - the actor to pursue + +Follow +------ + +Follow another actor. + +**Arguments** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - name + - type + - description + * - target + - `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 `_ [required] + - the actor to follow + * - destPosition + - `3d vector `_ [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 `_ [required] + - the point to travel to + diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index ef831e734a..41fd52253f 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -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 ` - `Game object reference `_ - `Cell reference `_ +- :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 ` | by local scripts | | Control basic AI of NPCs and creatures. | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`Camera ` | by player scripts | | Allows to alter behavior of the built-in camera script | | | | | without overriding the script completely. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst new file mode 100644 index 0000000000..3d443b2811 --- /dev/null +++ b/docs/source/reference/lua-scripting/events.rst @@ -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') + diff --git a/docs/source/reference/lua-scripting/interface_ai.rst b/docs/source/reference/lua-scripting/interface_ai.rst new file mode 100644 index 0000000000..ec79b50d5d --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_ai.rst @@ -0,0 +1,6 @@ +Interface AI +============ + +.. raw:: html + :file: generated_html/scripts_omw_ai.html + diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index e38e8948ec..f270def2fa 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -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 ` | by local scripts | | Control basic AI of NPCs and creatures. | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`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 ` that are used by built-in scripts. + Timers ====== diff --git a/files/builtin_scripts/builtin.omwscripts b/files/builtin_scripts/builtin.omwscripts index 30fccad9fa..2ffe7007f2 100644 --- a/files/builtin_scripts/builtin.omwscripts +++ b/files/builtin_scripts/builtin.omwscripts @@ -1 +1,2 @@ PLAYER: scripts/omw/camera.lua +NPC,CREATURE: scripts/omw/ai.lua diff --git a/files/builtin_scripts/scripts/omw/ai.lua b/files/builtin_scripts/scripts/omw/ai.lua new file mode 100644 index 0000000000..5d76a7d9be --- /dev/null +++ b/files/builtin_scripts/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 + 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, + }, +} + diff --git a/files/lua_api/openmw/self.lua b/files/lua_api/openmw/self.lua index 1228a92a87..d6f01412eb 100644 --- a/files/lua_api/openmw/self.lua +++ b/files/lua_api/openmw/self.lua @@ -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