diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 04df6b6b16..a25f1db124 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -84,11 +84,34 @@ namespace MWLua // api["resume"] = []() {}; } + static sol::table initContentFilesBindings(sol::state_view& lua) + { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + sol::table list(lua, sol::create); + for (size_t i = 0; i < contentList.size(); ++i) + list[i + 1] = Misc::StringUtils::lowerCase(contentList[i]); + sol::table res(lua, sol::create); + res["list"] = LuaUtil::makeReadOnly(list); + res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return i + 1; + return sol::nullopt; + }; + res["has"] = [&contentList](std::string_view contentFile) -> bool { + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return true; + return false; + }; + return LuaUtil::makeReadOnly(res); + } + static sol::table initCorePackage(const Context& context) { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 38; + api["API_REVISION"] = 39; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); MWBase::Environment::get().getStateManager()->requestQuit(); @@ -97,6 +120,14 @@ namespace MWLua context.mLuaEvents->addGlobalEvent( { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); }; + api["contentFiles"] = initContentFilesBindings(lua->sol()); + api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + for (size_t i = 0; i < contentList.size(); ++i) + if (Misc::StringUtils::ciEqual(contentList[i], contentFile)) + return ESM::RefId(ESM::FormIdRefId(ESM::FormId{ index, int(i) })).serializeText(); + throw std::runtime_error("Content file not found: " + std::string(contentFile)); + }; addTimeBindings(api, context, false); api["magic"] = initCoreMagicBindings(context); api["l10n"] = LuaUtil::initL10nLoader(lua->sol(), MWBase::Environment::get().getL10nManager()); @@ -189,6 +220,12 @@ namespace MWLua MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, *cell, count.value_or(1)); return GObject(newPtr); }; + api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject { + ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); + if (!refId.is()) + throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); + return GObject(refId.getIf()->getValue()); + }; // Creates a new record in the world database. api["createRecord"] = sol::overload( diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index c9f9163648..88c4907d81 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -124,6 +124,13 @@ namespace MWLua }); }; + api["getObjectByFormId"] = [](std::string_view formIdStr) -> LObject { + ESM::RefId refId = ESM::RefId::deserializeText(formIdStr); + if (!refId.is()) + throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId"); + return LObject(refId.getIf()->getValue()); + }; + api["activators"] = LObjectList{ worldView->getActivatorsInScene() }; api["actors"] = LObjectList{ worldView->getActorsInScene() }; api["containers"] = LObjectList{ worldView->getContainersInScene() }; diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index 01ef08ffdb..d72d778688 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -145,6 +145,13 @@ namespace MWLua void addBasicBindings(sol::usertype& objectT, const Context& context) { objectT["id"] = sol::readonly_property([](const ObjectT& o) -> std::string { return o.id().toString(); }); + objectT["contentFile"] = sol::readonly_property([](const ObjectT& o) -> sol::optional { + int contentFileIndex = o.id().mContentFile; + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + if (contentFileIndex < 0 || contentFileIndex >= static_cast(contentList.size())) + return sol::nullopt; + return Misc::StringUtils::lowerCase(contentList[contentFileIndex]); + }); objectT["isValid"] = [](const ObjectT& o) { return !o.ptrOrNull().isEmpty(); }; objectT["recordId"] = sol::readonly_property( [](const ObjectT& o) -> std::string { return o.ptr().getCellRef().getRefId().serializeText(); }); diff --git a/apps/openmw/mwlua/types/door.cpp b/apps/openmw/mwlua/types/door.cpp index e537f7c377..f13952a6b5 100644 --- a/apps/openmw/mwlua/types/door.cpp +++ b/apps/openmw/mwlua/types/door.cpp @@ -106,5 +106,7 @@ namespace MWLua record["model"] = sol::readonly_property([vfs](const ESM4::Door& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel, vfs); }); + record["isAutomatic"] = sol::readonly_property( + [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); } } diff --git a/apps/openmw/mwlua/worldview.cpp b/apps/openmw/mwlua/worldview.cpp index f5699dff89..6d8c01c56d 100644 --- a/apps/openmw/mwlua/worldview.cpp +++ b/apps/openmw/mwlua/worldview.cpp @@ -47,7 +47,7 @@ namespace MWLua return &mActivatorsInScene; if (cls.isActor()) return &mActorsInScene; - if (cls.isDoor()) + if (ptr.mRef->getType() == ESM::REC_DOOR || ptr.mRef->getType() == ESM::REC_DOOR4) return &mDoorsInScene; if (typeid(cls) == typeid(MWClass::Container)) return &mContainersInScene; diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index bc6d4b0055..eb61f3c118 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -74,6 +74,7 @@ set(BUILTIN_DATA_FILES scripts/omw/console/player.lua scripts/omw/console/global.lua scripts/omw/console/local.lua + scripts/omw/mechanics/playercontroller.lua scripts/omw/playercontrols.lua scripts/omw/settings/player.lua scripts/omw/settings/global.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index 46a1f3f32f..cbc94496cd 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -7,6 +7,7 @@ PLAYER: scripts/omw/settings/player.lua # Mechanics GLOBAL: scripts/omw/activationhandlers.lua +PLAYER: scripts/omw/mechanics/playercontroller.lua PLAYER: scripts/omw/playercontrols.lua PLAYER: scripts/omw/camera/camera.lua NPC,CREATURE: scripts/omw/ai.lua diff --git a/files/data/scripts/omw/mechanics/playercontroller.lua b/files/data/scripts/omw/mechanics/playercontroller.lua new file mode 100644 index 0000000000..19e62d02c7 --- /dev/null +++ b/files/data/scripts/omw/mechanics/playercontroller.lua @@ -0,0 +1,46 @@ +local core = require('openmw.core') +local nearby = require('openmw.nearby') +local self = require('openmw.self') +local types = require('openmw.types') + +local cell = nil +local autodoors = {} + +local function onCellChange() + autodoors = {} + for _, door in ipairs(nearby.doors) do + if door.type == types.ESM4Door and types.ESM4Door.record(door).isAutomatic then + autodoors[#autodoors + 1] = door + end + end +end + +local autodoorActivationDist = 300 + +local lastAutoActivation = 0 +local function processAutomaticDoors() + if core.getRealTime() - lastAutoActivation < 2 then + return + end + for _, door in ipairs(autodoors) do + if door.enabled and (door.position - self.position):length() < autodoorActivationDist then + print('Automatic activation of', door) + door:activateBy(self) + lastAutoActivation = core.getRealTime() + end + end +end + +local function onUpdate() + if self.cell ~= cell then + cell = self.cell + onCellChange() + end + processAutomaticDoors() +end + +return { + engineHandlers = { + onUpdate = onUpdate, + }, +} diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index fa56a1c80e..eefdc322e6 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -102,12 +102,53 @@ -- print( myMsg('Hello {name}!', {name='World'}) ) +--- +-- @{#ContentFiles}: functions working with the list of currently loaded content files. +-- @field [parent=#core] #ContentFiles contentFiles + +--- +-- Functions working with the list of currently loaded content files. +-- @type ContentFiles +-- @field #list<#string> list The current load order (list of content file names). + +--- +-- Return the index of a specific content file in the load order (or `nil` if there is no such content file). +-- @function [parent=#ContentFiles] indexOf +-- @param #string contentFile +-- @return #number + +--- +-- Check if the content file with given name present in the load order. +-- @function [parent=#ContentFiles] has +-- @param #string contentFile +-- @return #boolean + +--- +-- Construct FormId string from content file name and the index in the file. +-- In ESM3 games (e.g. Morrowind) FormIds are used to reference game objects. +-- In ESM4 games (e.g. Skyrim) FormIds are used both for game objects and as record ids. +-- @function [parent=#core] getFormId +-- @param #string contentFile +-- @param #number index +-- @return #string +-- @usage if obj.recordId == core.getFormId('Skyrim.esm', 0x4d7da) then ... end +-- @usage -- In ESM3 content files (e.g. Morrowind) ids are human-readable strings +-- obj.ownerFactionId = 'blades' +-- -- In ESM4 (e.g. Skyrim) ids should be constructed using `core.getFormId`: +-- obj.ownerFactionId = core.getFormId('Skyrim.esm', 0x72834) +-- @usage -- local scripts +-- local obj = nearby.getObjectByFormId(core.getFormId('Morrowind.esm', 128964)) +-- @usage -- global scripts +-- local obj = world.getObjectByFormId(core.getFormId('Morrowind.esm', 128964)) + + --- -- Any object that exists in the game world and has a specific location. -- Player, actors, items, and statics are game objects. -- @type GameObject -- @extends #userdata -- @field #string id A unique id of this object (not record id), can be used as a key in a table. +-- @field #string contentFile Lower cased file name of the content file that defines this object; nil for dynamically created objects. -- @field #boolean enabled Whether the object is enabled or disabled. Global scripts can set the value. Items in containers or inventories can't be disabled. -- @field openmw.util#Vector3 position Object position. -- @field openmw.util#Vector3 rotation Object rotation (ZXY order). diff --git a/files/lua_api/openmw/nearby.lua b/files/lua_api/openmw/nearby.lua index e28f867d38..bec284a10b 100644 --- a/files/lua_api/openmw/nearby.lua +++ b/files/lua_api/openmw/nearby.lua @@ -26,6 +26,16 @@ -- Everything that can be picked up in the nearby. -- @field [parent=#nearby] openmw.core#ObjectList items +--- +-- Return an object by RefNum/FormId. +-- Note: the function always returns @{openmw.core#GameObject} and doesn't validate that +-- the object exists in the game world. If it doesn't exist or not yet loaded to memory), +-- then `obj:isValid()` will be `false`. +-- @function [parent=#nearby] getObjectByFormId +-- @param #string formId String returned by `core.getFormId` +-- @return openmw.core#GameObject +-- @usage local obj = nearby.getObjectByFormId(core.getFormId('Morrowind.esm', 128964)) + --- -- @type COLLISION_TYPE -- @field [parent=#COLLISION_TYPE] #number World diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 2cc80063fd..5f7a46f284 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -65,6 +65,16 @@ -- @function [parent=#world] isWorldPaused -- @return #boolean +--- +-- Return an object by RefNum/FormId. +-- Note: the function always returns @{openmw.core#GameObject} and doesn't validate that +-- the object exists in the game world. If it doesn't exist or not yet loaded to memory), +-- then `obj:isValid()` will be `false`. +-- @function [parent=#world] getObjectByFormId +-- @param #string formId String returned by `core.getFormId` +-- @return openmw.core#GameObject +-- @usage local obj = world.getObjectByFormId(core.getFormId('Morrowind.esm', 128964)) + --- -- Create a new instance of the given record. -- After creation the object is in the disabled state. Use :teleport to place to the world or :moveInto to put it into a container or an inventory.