Merge branch 'landscape-data-bindings' into 'master'

Landscape height and texture bindings for Lua (Revised and Reopened)

See merge request OpenMW/openmw!4724
This commit is contained in:
psi29a 2025-07-02 21:53:05 +00:00
commit 53feb29a5b
6 changed files with 162 additions and 2 deletions

View File

@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 50)
set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 77)
set(OPENMW_LUA_API_REVISION 78)
set(OPENMW_POSTPROCESSING_API_REVISION 2)
set(OPENMW_VERSION_COMMITHASH "")

View File

@ -62,7 +62,7 @@ add_openmw_dir (mwlua
luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant
context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings coremwscriptbindings
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

View File

@ -22,6 +22,7 @@
#include "coremwscriptbindings.hpp"
#include "dialoguebindings.hpp"
#include "factionbindings.hpp"
#include "landbindings.hpp"
#include "luaevents.hpp"
#include "magicbindings.hpp"
#include "soundbindings.hpp"
@ -101,6 +102,8 @@ namespace MWLua
api["mwscripts"]
= context.cachePackage("openmw_core_mwscripts", [context]() { return initCoreMwScriptBindings(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"]

View File

@ -0,0 +1,124 @@
#include "landbindings.hpp"
#include <components/esm/refid.hpp>
#include <components/esm/util.hpp>
#include <components/esmterrain/storage.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/worldmodel.hpp"
#include "object.hpp"
namespace
{
// Takes in a corrected world pos to match the visuals.
ESMTerrain::UniqueTextureId getTextureAt(const std::span<const std::uint16_t> landData, const int plugin,
const osg::Vec3f& correctedWorldPos, const float cellSize)
{
int cellX = static_cast<int>(std::floor(correctedWorldPos.x() / cellSize));
int cellY = static_cast<int>(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<int>(nX * ESM::Land::LAND_TEXTURE_SIZE);
int startY = static_cast<int>(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 cellOrId)
{
const MWWorld::Cell* cell = nullptr;
if (cellOrId.is<MWLua::GCell>())
cell = cellOrId.as<MWLua::GCell>().mStore->getCell();
else if (cellOrId.is<MWLua::LCell>())
cell = cellOrId.as<MWLua::LCell>().mStore->getCell();
else if (cellOrId.is<std::string_view>() && !cellOrId.as<std::string_view>().empty())
cell = MWBase::Environment::get()
.getWorldModel()
->getCell(ESM::RefId::deserializeText(cellOrId.as<std::string_view>()))
.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 cellOrId) {
ESM::RefId worldspace = worldspaceAt(cellOrId);
return MWBase::Environment::get().getWorld()->getTerrainHeightAt(pos, worldspace);
};
landApi["getTextureAt"] = [lua = lua](const osg::Vec3f& pos, sol::object cellOrId) {
sol::variadic_results values;
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
const MWWorld::Store<ESM::Land>& landStore = store.get<ESM::Land>();
ESM::RefId worldspace = worldspaceAt(cellOrId);
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<int>(std::floor(correctedPos.x() / cellSize));
int cellY = static_cast<int>(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<ESM::LandTexture>& textureStore = store.get<ESM::LandTexture>();
const std::string* textureString = textureStore.search(textureId.first - 1, textureId.second);
if (!textureString)
return values;
values.push_back(sol::make_object<std::string>(lua, *textureString));
const std::vector<std::string>& contentList = MWBase::Environment::get().getWorld()->getContentFiles();
if (textureId.second >= 0 && static_cast<size_t>(textureId.second) < contentList.size())
values.push_back(sol::make_object<std::string>(lua, contentList[textureId.second]));
}
return values;
};
return LuaUtil::makeReadOnly(landApi);
}
}

View File

@ -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

View File

@ -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 cellOrId cell or cell id 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 cellOrId cell or cell id 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