diff --git a/CHANGELOG.md b/CHANGELOG.md index 479a19d905..6310b9dbb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +0.50.0 +------ + + Feature #8112: Expose landscape record data to Lua + 0.49.0 ------ diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 37de0abeab..48dcf41aae 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -62,7 +62,7 @@ add_openmw_dir (mwlua luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings dialoguebindings - postprocessingbindings stats recordstore debugbindings corebindings worldbindings worker magicbindings factionbindings + postprocessingbindings stats recordstore debugbindings corebindings worldbindings worker landbindings magicbindings factionbindings classbindings itemdata inputprocessor animationbindings birthsignbindings racebindings markupbindings types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus diff --git a/apps/openmw/mwlua/corebindings.cpp b/apps/openmw/mwlua/corebindings.cpp index 9df435c00d..2c3b0475ba 100644 --- a/apps/openmw/mwlua/corebindings.cpp +++ b/apps/openmw/mwlua/corebindings.cpp @@ -21,6 +21,7 @@ #include "dialoguebindings.hpp" #include "factionbindings.hpp" +#include "landbindings.hpp" #include "luaevents.hpp" #include "magicbindings.hpp" #include "soundbindings.hpp" @@ -97,6 +98,8 @@ namespace MWLua api["stats"] = context.cachePackage("openmw_core_stats", [context]() { return initCoreStatsBindings(context); }); + api["land"] = context.cachePackage("openmw_core_land", [context]() { return initCoreLandBindings(context); }); + api["factions"] = context.cachePackage("openmw_core_factions", [context]() { return initCoreFactionBindings(context); }); api["dialogue"] diff --git a/apps/openmw/mwlua/landbindings.cpp b/apps/openmw/mwlua/landbindings.cpp new file mode 100644 index 0000000000..a4ebc0a841 --- /dev/null +++ b/apps/openmw/mwlua/landbindings.cpp @@ -0,0 +1,121 @@ +#include "landbindings.hpp" + +#include +#include +#include + +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwworld/esmstore.hpp" + +namespace +{ + // Takes in a corrected world pos to match the visuals. + ESMTerrain::UniqueTextureId getTextureAt(const std::span landData, const int plugin, + const osg::Vec3f& correctedWorldPos, const float cellSize) + { + int cellX = static_cast(std::floor(correctedWorldPos.x() / cellSize)); + int cellY = static_cast(std::floor(correctedWorldPos.y() / cellSize)); + + // Normalized position in the cell + float nX = (correctedWorldPos.x() - (cellX * cellSize)) / cellSize; + float nY = (correctedWorldPos.y() - (cellY * cellSize)) / cellSize; + + int startX = static_cast(nX * ESM::Land::LAND_TEXTURE_SIZE); + int startY = static_cast(nY * ESM::Land::LAND_TEXTURE_SIZE); + + assert(startX < ESM::Land::LAND_TEXTURE_SIZE); + assert(startY < ESM::Land::LAND_TEXTURE_SIZE); + + const std::uint16_t tex = landData[startY * ESM::Land::LAND_TEXTURE_SIZE + startX]; + if (tex == 0) + return { 0, 0 }; // vtex 0 is always the base texture, regardless of plugin + + return { tex, plugin }; + } + + const ESM::RefId worldspaceAt(sol::object cellOrName) + { + const MWWorld::Cell* cell = nullptr; + if (cellOrName.is()) + cell = cellOrName.as().mStore->getCell(); + else if (cellOrName.is()) + cell = cellOrName.as().mStore->getCell(); + else if (cellOrName.is() && !cellOrName.as().empty()) + cell = MWBase::Environment::get().getWorldModel()->getCell(cellOrName.as()).getCell(); + if (cell == nullptr) + throw std::runtime_error("Invalid cell"); + else if (!cell->isExterior()) + throw std::runtime_error("Cell cannot be interior"); + + return cell->getWorldSpace(); + } +} + +namespace MWLua +{ + sol::table initCoreLandBindings(const Context& context) + { + sol::state_view lua = context.sol(); + sol::table landApi(lua, sol::create); + + landApi["getHeightAt"] = [](const osg::Vec3f& pos, sol::object cellOrName) { + ESM::RefId worldspace = worldspaceAt(cellOrName); + return MWBase::Environment::get().getWorld()->getTerrainHeightAt(pos, worldspace); + }; + + landApi["getTextureAt"] = [lua = lua](const osg::Vec3f& pos, sol::object cellOrName) { + sol::variadic_results values; + const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); + const MWWorld::Store& landStore = store.get(); + ESM::RefId worldspace = worldspaceAt(cellOrName); + + if (worldspace != ESM::Cell::sDefaultWorldspaceId) + return values; + + const float cellSize = ESM::getCellSize(worldspace); + const float offset = (cellSize / ESM::LandRecordData::sLandTextureSize) * 0.25; + const osg::Vec3f correctedPos = pos + osg::Vec3f{ -offset, +offset, 0.0f }; + + const ESM::Land* land = nullptr; + const ESM::Land::LandData* landData = nullptr; + + int cellX = static_cast(std::floor(correctedPos.x() / cellSize)); + int cellY = static_cast(std::floor(correctedPos.y() / cellSize)); + + land = landStore.search(cellX, cellY); + + if (land != nullptr) + landData = land->getLandData(ESM::Land::DATA_VTEX); + + // If we fail to preload land data, return, we need to be able to get *any* land to know how to correct + // the position used to sample terrain + if (landData == nullptr) + return values; + + const ESMTerrain::UniqueTextureId textureId + = getTextureAt(landData->mTextures, land->getPlugin(), correctedPos, cellSize); + + // Need to check for 0, 0 so that we can safely subtract 1 later, as per documentation on UniqueTextureId + if (textureId.first != 0) + { + const MWWorld::Store& textureStore = store.get(); + const std::string* textureString = textureStore.search(textureId.first - 1, textureId.second); + if (!textureString) + return values; + + values.push_back(sol::make_object(lua, *textureString)); + const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); + if (textureId.second >= 0 && static_cast(textureId.second) < contentList.size()) + values.push_back(sol::make_object(lua, contentList[textureId.second])); + } + + return values; + }; + + return LuaUtil::makeReadOnly(landApi); + } +} diff --git a/apps/openmw/mwlua/landbindings.hpp b/apps/openmw/mwlua/landbindings.hpp new file mode 100644 index 0000000000..8cdf30046b --- /dev/null +++ b/apps/openmw/mwlua/landbindings.hpp @@ -0,0 +1,11 @@ +#ifndef MWLUA_LANDBINDINGS_H +#define MWLUA_LANDBINDINGS_H + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreLandBindings(const Context& context); +} + +#endif // MWLUA_LANDBINDINGS_H diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 8a78a52441..7a212a10ea 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -450,6 +450,28 @@ -- @usage for _, item in ipairs(inventory:findAll('common_shirt_01')) do ... end +--- @{#Land}: Functions for interacting with land data +-- @field [parent=#core] #Land land + +--- +-- Get the terrain height at a given location. +-- @function [parent=#Land] getHeightAt +-- @param openmw.util#Vector3 position +-- @param #any cellOrName (optional) cell or cell name in their exterior world space to query +-- @return #number + +--- +-- Get the terrain texture at a given location. As textures are blended and +-- multiple textures can be at one specific position the texture whose center is +-- closest to the position will be returned. +-- +-- @function [parent=#Land] getTextureAt +-- @param openmw.util#Vector3 position +-- @param #any cellOrName cell or cell name in their exterior world space to query +-- @return #nil, #string Texture path or nil if one isn't defined +-- @return #nil, #string Plugin name or nil if failed to retrieve the texture + + --- @{#Magic}: spells and spell effects -- @field [parent=#core] #Magic magic