diff --git a/CMakeLists.txt b/CMakeLists.txt index 47e50e769a..d1cc16b8b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,7 +81,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 60) +set(OPENMW_LUA_API_REVISION 61) set(OPENMW_POSTPROCESSING_API_REVISION 1) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8fa0571afc..3b0d44a984 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -104,7 +104,27 @@ namespace MWLua }); aiPackage["sideWithTarget"] = sol::readonly_property([](const AiPackage& p) { return p.sideWithTarget(); }); aiPackage["destPosition"] = sol::readonly_property([](const AiPackage& p) { return p.getDestination(); }); + aiPackage["distance"] = sol::readonly_property([](const AiPackage& p) { return p.getDistance(); }); + aiPackage["duration"] = sol::readonly_property([](const AiPackage& p) { return p.getDuration(); }); + aiPackage["idle"] = sol::readonly_property([context](const AiPackage& p) -> sol::optional { + if (p.getTypeId() == MWMechanics::AiPackageTypeId::Wander) + { + sol::table idles(context.mLua->sol(), sol::create); + const std::vector& idle = static_cast(p).getIdle(); + if (!idle.empty()) + { + for (size_t i = 0; i < idle.size(); ++i) + { + std::string_view groupName = MWMechanics::AiWander::getIdleGroupName(i); + idles[groupName] = idle[i]; + } + return idles; + } + } + return sol::nullopt; + }); + aiPackage["isRepeat"] = sol::readonly_property([](const AiPackage& p) { return p.getRepeat(); }); selfAPI["_getActiveAiPackage"] = [](SelfObject& self) -> sol::optional> { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); @@ -132,13 +152,25 @@ namespace MWLua MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); ai.stack(MWMechanics::AiPursue(target.ptr()), ptr, cancelOther); }; - selfAPI["_startAiFollow"] = [](SelfObject& self, const LObject& target, bool cancelOther) { + selfAPI["_startAiFollow"] = [](SelfObject& self, const LObject& target, sol::optional cell, + float duration, const osg::Vec3f& dest, bool repeat, bool cancelOther) { const MWWorld::Ptr& ptr = self.ptr(); MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - ai.stack(MWMechanics::AiFollow(target.ptr()), ptr, cancelOther); + if (cell) + { + ai.stack(MWMechanics::AiFollow(target.ptr().getCellRef().getRefId(), + cell->mStore->getCell()->getNameId(), duration, dest.x(), dest.y(), dest.z(), repeat), + ptr, cancelOther); + } + else + { + ai.stack(MWMechanics::AiFollow( + target.ptr().getCellRef().getRefId(), duration, dest.x(), dest.y(), dest.z(), repeat), + ptr, cancelOther); + } }; selfAPI["_startAiEscort"] = [](SelfObject& self, const LObject& target, LCell cell, float duration, - const osg::Vec3f& dest, bool cancelOther) { + const osg::Vec3f& dest, bool repeat, bool cancelOther) { 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. @@ -146,23 +178,27 @@ namespace MWLua int gameHoursDuration = static_cast(std::ceil(duration / 3600.0)); auto* esmCell = cell.mStore->getCell(); if (esmCell->isExterior()) - ai.stack(MWMechanics::AiEscort(refId, gameHoursDuration, dest.x(), dest.y(), dest.z(), false), ptr, + ai.stack(MWMechanics::AiEscort(refId, gameHoursDuration, dest.x(), dest.y(), dest.z(), repeat), ptr, cancelOther); else ai.stack(MWMechanics::AiEscort( - refId, esmCell->getNameId(), gameHoursDuration, dest.x(), dest.y(), dest.z(), false), + refId, esmCell->getNameId(), gameHoursDuration, dest.x(), dest.y(), dest.z(), repeat), ptr, cancelOther); }; - selfAPI["_startAiWander"] = [](SelfObject& self, int distance, float duration, bool cancelOther) { + selfAPI["_startAiWander"] + = [](SelfObject& self, int distance, int duration, sol::table luaIdle, bool repeat, bool cancelOther) { + const MWWorld::Ptr& ptr = self.ptr(); + MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + std::vector idle; + // Lua index starts at 1 + for (size_t i = 1; i <= luaIdle.size(); i++) + idle.emplace_back(luaIdle.get(i)); + ai.stack(MWMechanics::AiWander(distance, duration, 0, idle, repeat), ptr, cancelOther); + }; + selfAPI["_startAiTravel"] = [](SelfObject& self, const osg::Vec3f& target, bool repeat, bool cancelOther) { 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, cancelOther); - }; - selfAPI["_startAiTravel"] = [](SelfObject& self, const osg::Vec3f& target, bool cancelOther) { - 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, cancelOther); + ai.stack(MWMechanics::AiTravel(target.x(), target.y(), target.z(), repeat), ptr, cancelOther); }; selfAPI["_enableLuaAnimations"] = [](SelfObject& self, bool enable) { const MWWorld::Ptr& ptr = self.ptr(); diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 709b2bee59..d88ecac6a5 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -51,6 +51,8 @@ namespace MWMechanics osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } + std::optional getDuration() const override { return mDuration; } + private: const std::string mCellId; const float mX; diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 29a9f9c9ad..ca33f5dc90 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -110,6 +110,10 @@ namespace MWMechanics virtual osg::Vec3f getDestination() const { return osg::Vec3f(0, 0, 0); } + virtual std::optional getDistance() const { return std::nullopt; } + + virtual std::optional getDuration() const { return std::nullopt; } + /// Return true if any loaded actor with this AI package must be active. bool alwaysActive() const { return mOptions.mAlwaysActive; } diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 6d5bd7f8cd..aed7214f4d 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -113,6 +113,14 @@ namespace MWMechanics bool isStationary() const { return mDistance == 0; } + std::optional getDistance() const override { return mDistance; } + + std::optional getDuration() const override { return static_cast(mDuration); } + + const std::vector& getIdle() const { return mIdle; } + + static std::string_view getIdleGroupName(size_t index) { return sIdleSelectToGroupName[index]; } + private: void stopWalking(const MWWorld::Ptr& actor); diff --git a/docs/source/reference/lua-scripting/aipackages.rst b/docs/source/reference/lua-scripting/aipackages.rst index 4ef3149582..7a23d156f5 100644 --- a/docs/source/reference/lua-scripting/aipackages.rst +++ b/docs/source/reference/lua-scripting/aipackages.rst @@ -99,6 +99,18 @@ Follow another actor. * - target - `GameObject `_ [required] - the actor to follow + * - destCell + - Cell [optional] + - the destination cell + * - duration + - number [optional] + - duration in game time (will be rounded up to the next hour) + * - destPosition + - `3d vector `_ [optional] + - the destination point + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) Escort ------ @@ -126,6 +138,9 @@ Escort another actor to the given location. * - duration - number [optional] - duration in game time (will be rounded up to the next hour) + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) **Example** @@ -136,6 +151,7 @@ Escort another actor to the given location. target = object.self, destPosition = util.vector3(x, y, z), duration = 3 * time.hour, + isRepeat = true }) Wander @@ -158,6 +174,34 @@ Wander nearby current position. * - duration - number [optional] - duration in game time (will be rounded up to the next hour) + * - idle + - table [optional] + - Idle chance values, up to 8 + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) + +**Example** + +.. code-block:: Lua + + local idleTable = { + idle2 = 60, + idle3 = 50, + idle4 = 40, + idle5 = 30, + idle6 = 20, + idle7 = 10, + idle8 = 0, + idle9 = 25 + } + actor:sendEvent('StartAIPackage', { + type = 'Wander', + distance = 5000, + duration = 5 * time.hour, + idle = idleTable, + isRepeat = true + }) Travel ------ @@ -176,4 +220,6 @@ Go to given location. * - destPosition - `3d vector `_ [required] - the point to travel to - + * - isRepeat + - boolean [optional] + - Will the package repeat (true or false) diff --git a/files/data/scripts/omw/ai.lua b/files/data/scripts/omw/ai.lua index 0bf9e1bbec..f90b92fd87 100644 --- a/files/data/scripts/omw/ai.lua +++ b/files/data/scripts/omw/ai.lua @@ -1,5 +1,6 @@ local self = require('openmw.self') local interfaces = require('openmw.interfaces') +local util = require('openmw.util') local function startPackage(args) local cancelOther = args.cancelOther @@ -12,16 +13,34 @@ local function startPackage(args) self:_startAiPursue(args.target, cancelOther) elseif args.type == 'Follow' then if not args.target then error("target required") end - self:_startAiFollow(args.target, cancelOther) + self:_startAiFollow(args.target, args.cellId, args.duration or 0, args.destPosition or util.vector3(0, 0, 0), args.isRepeat or false, cancelOther) 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, cancelOther) elseif args.type == 'Wander' then - self:_startAiWander(args.distance or 0, args.duration or 0, cancelOther) + local key = "idle" + local idle = {} + local duration = 0 + for i = 2, 9 do + local val = args.idle[key .. i] + if val == nil then + idle[i-1] = 0 + else + local v = tonumber(val) or 0 + if v < 0 or v > 100 then + error("idle values cannot exceed 100") + end + idle[i-1] = v + end + end + if args.duration then + duration = args.duration / 3600 + end + self:_startAiWander(args.distance or 0, duration, idle, args.isRepeat or false, cancelOther) elseif args.type == 'Travel' then if not args.destPosition then error("destPosition required") end - self:_startAiTravel(args.destPosition, cancelOther) + self:_startAiTravel(args.destPosition, args.isRepeat or false, cancelOther) else error('Unsupported AI Package: ' .. args.type) end @@ -47,6 +66,10 @@ return { -- @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 destPosition Destination point of the AI package. + -- @field #number distance Distance value (can be nil). + -- @field #number duration Duration value (can be nil). + -- @field #table idle Idle value (can be nil). + -- @field #boolean isRepeat Should this package be repeated (true or false). --- Return the currently active AI package (or `nil` if there are no AI packages). -- @function [parent=#AI] getActivePackage