Merge branch 'fargoths_lost_weather_ring' into 'master'

Make most weather bindings read/write

See merge request OpenMW/openmw!4866
This commit is contained in:
Alexei Kotov 2025-08-28 03:22:34 +03:00
commit 15b9d70cbe
15 changed files with 264 additions and 151 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 94)
set(OPENMW_LUA_API_REVISION 95)
set(OPENMW_POSTPROCESSING_API_REVISION 3)
set(OPENMW_VERSION_COMMITHASH "")

View File

@ -1,7 +1,7 @@
#include "animationbindings.hpp"
#include <components/lua/luastate.hpp>
#include <components/misc/finitenumbers.hpp>
#include <components/misc/finitevalues.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"

View File

@ -2,7 +2,7 @@
#include <components/lua/luastate.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/misc/finitenumbers.hpp>
#include <components/misc/finitevalues.hpp>
#include <components/settings/values.hpp>
#include "../mwbase/environment.hpp"

View File

@ -7,6 +7,7 @@
#include <components/esm3/loadregn.hpp>
#include <components/lua/util.hpp>
#include <components/misc/color.hpp>
#include <components/misc/finitevalues.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -20,6 +21,18 @@
#include "context.hpp"
#include "object.hpp"
namespace sol
{
template <>
struct is_automagical<MWWorld::TimeOfDayInterpolator<float>> : std::false_type
{
};
template <>
struct is_automagical<MWWorld::TimeOfDayInterpolator<osg::Vec4f>> : std::false_type
{
};
}
namespace
{
class WeatherStore
@ -36,11 +49,6 @@ namespace
size_t size() const { return MWBase::Environment::get().getWorld()->getAllWeather().size(); }
};
Misc::Color color(const osg::Vec4f& color)
{
return Misc::Color(color.r(), color.g(), color.b(), color.a());
}
template <class Cell>
bool hasWeather(const Cell& cell, bool requireExterior)
{
@ -77,110 +85,146 @@ namespace
return (world.*getter)();
});
}
void createFloatInterpolator(sol::state_view lua)
{
using Misc::FiniteFloat;
using T = MWWorld::TimeOfDayInterpolator<float>;
auto interT = lua.new_usertype<T>("TimeOfDayInterpolatorFloat");
interT["sunrise"] = sol::property([](const T& inter) { return inter.getSunriseValue(); },
[](T& inter, FiniteFloat value) { inter.setSunriseValue(value); });
interT["sunset"] = sol::property([](const T& inter) { return inter.getSunsetValue(); },
[](T& inter, FiniteFloat value) { inter.setSunsetValue(value); });
interT["day"] = sol::property([](const T& inter) { return inter.getDayValue(); },
[](T& inter, FiniteFloat value) { inter.setDayValue(value); });
interT["night"] = sol::property([](const T& inter) { return inter.getNightValue(); },
[](T& inter, FiniteFloat value) { inter.setNightValue(value); });
}
void createColorInterpolator(sol::state_view lua)
{
using Misc::Color;
using T = MWWorld::TimeOfDayInterpolator<osg::Vec4f>;
auto interT = lua.new_usertype<T>("TimeOfDayInterpolatorColor");
interT["sunrise"] = sol::property([](const T& inter) { return Color::fromVec(inter.getSunriseValue()); },
[](T& inter, const Color& value) { inter.setSunriseValue(value.toVec()); });
interT["sunset"] = sol::property([](const T& inter) { return Color::fromVec(inter.getSunsetValue()); },
[](T& inter, const Color& value) { inter.setSunsetValue(value.toVec()); });
interT["day"] = sol::property([](const T& inter) { return Color::fromVec(inter.getDayValue()); },
[](T& inter, const Color& value) { inter.setDayValue(value.toVec()); });
interT["night"] = sol::property([](const T& inter) { return Color::fromVec(inter.getNightValue()); },
[](T& inter, const Color& value) { inter.setNightValue(value.toVec()); });
}
}
namespace MWLua
{
sol::table initCoreWeatherBindings(const Context& context)
{
using Misc::FiniteFloat;
using Misc::FiniteVec3f;
sol::state_view lua = context.sol();
sol::table api(lua, sol::create);
auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
auto weatherT = lua.new_usertype<MWWorld::Weather>("Weather");
createFloatInterpolator(lua);
createColorInterpolator(lua);
weatherT[sol::meta_function::to_string]
= [](const MWWorld::Weather& w) -> std::string { return "Weather[" + w.mName + "]"; };
weatherT["name"]
= sol::readonly_property([](const MWWorld::Weather& w) -> std::string_view { return w.mName; });
weatherT["windSpeed"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mWindSpeed; });
weatherT["cloudSpeed"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mCloudSpeed; });
weatherT["cloudTexture"] = sol::readonly_property([vfs](const MWWorld::Weather& w) {
return Misc::ResourceHelpers::correctTexturePath(w.mCloudTexture, vfs);
});
weatherT["cloudsMaximumPercent"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mCloudsMaximumPercent; });
weatherT["isStorm"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mIsStorm; });
weatherT["stormDirection"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mStormDirection; });
weatherT["glareView"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mGlareView; });
weatherT["rainSpeed"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainSpeed; });
weatherT["rainEntranceSpeed"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainEntranceSpeed; });
weatherT["rainEffect"] = sol::readonly_property([](const MWWorld::Weather& w) -> sol::optional<std::string> {
if (w.mRainEffect.empty())
return sol::nullopt;
return w.mRainEffect;
});
weatherT["rainMaxRaindrops"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainMaxRaindrops; });
weatherT["rainDiameter"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainDiameter; });
weatherT["rainThreshold"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainThreshold; });
weatherT["rainMaxHeight"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainMaxHeight; });
weatherT["rainMinHeight"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainMinHeight; });
weatherT["rainLoopSoundID"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mRainLoopSoundID.serializeText(); });
weatherT["scriptId"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mScriptId; });
weatherT["recordId"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mId.serializeText(); });
weatherT["thunderSoundID"] = sol::readonly_property([lua](const MWWorld::Weather& w) {
sol::table result(lua, sol::create);
for (const auto& soundId : w.mThunderSoundID)
result.add(soundId.serializeText());
return result;
});
weatherT["windSpeed"] = sol::property([](const MWWorld::Weather& w) { return w.mWindSpeed; },
[](MWWorld::Weather& w, const FiniteFloat windSpeed) { w.mWindSpeed = windSpeed; });
weatherT["cloudSpeed"] = sol::property([](const MWWorld::Weather& w) { return w.mCloudSpeed; },
[](MWWorld::Weather& w, const FiniteFloat cloudSpeed) { w.mCloudSpeed = cloudSpeed; });
weatherT["cloudTexture"] = sol::property(
[vfs](
const MWWorld::Weather& w) { return Misc::ResourceHelpers::correctTexturePath(w.mCloudTexture, vfs); },
[](MWWorld::Weather& w, std::string_view cloudTexture) { w.mCloudTexture = cloudTexture; });
weatherT["cloudsMaximumPercent"]
= sol::property([](const MWWorld::Weather& w) { return w.mCloudsMaximumPercent; },
[](MWWorld::Weather& w, const FiniteFloat cloudsMaximumPercent) {
w.mCloudsMaximumPercent = cloudsMaximumPercent;
});
weatherT["isStorm"] = sol::property([](const MWWorld::Weather& w) { return w.mIsStorm; },
[](MWWorld::Weather& w, bool isStorm) { w.mIsStorm = isStorm; });
weatherT["stormDirection"] = sol::property([](const MWWorld::Weather& w) { return w.mStormDirection; },
[](MWWorld::Weather& w, const FiniteVec3f& stormDirection) { w.mStormDirection = stormDirection; });
weatherT["glareView"] = sol::property([](const MWWorld::Weather& w) { return w.mGlareView; },
[](MWWorld::Weather& w, const FiniteFloat glareView) { w.mGlareView = glareView; });
weatherT["rainSpeed"] = sol::property([](const MWWorld::Weather& w) { return w.mRainSpeed; },
[](MWWorld::Weather& w, const FiniteFloat rainSpeed) { w.mRainSpeed = rainSpeed; });
weatherT["rainEntranceSpeed"] = sol::property([](const MWWorld::Weather& w) { return w.mRainEntranceSpeed; },
[](MWWorld::Weather& w, const FiniteFloat rainEntranceSpeed) { w.mRainEntranceSpeed = rainEntranceSpeed; });
weatherT["rainEffect"] = sol::property(
[](const MWWorld::Weather& w) -> sol::optional<std::string> {
if (w.mRainEffect.empty())
return sol::nullopt;
return w.mRainEffect;
},
[](MWWorld::Weather& w, sol::optional<std::string_view> rainEffect) {
w.mRainEffect = rainEffect.value_or("");
});
weatherT["rainMaxRaindrops"] = sol::property([](const MWWorld::Weather& w) { return w.mRainMaxRaindrops; },
[](MWWorld::Weather& w, int rainMaxRaindrops) { w.mRainMaxRaindrops = rainMaxRaindrops; });
weatherT["rainDiameter"] = sol::property([](const MWWorld::Weather& w) { return w.mRainDiameter; },
[](MWWorld::Weather& w, const FiniteFloat rainDiameter) { w.mRainDiameter = rainDiameter; });
weatherT["rainThreshold"] = sol::property([](const MWWorld::Weather& w) { return w.mRainThreshold; },
[](MWWorld::Weather& w, const FiniteFloat rainThreshold) { w.mRainThreshold = rainThreshold; });
weatherT["rainMaxHeight"] = sol::property([](const MWWorld::Weather& w) { return w.mRainMaxHeight; },
[](MWWorld::Weather& w, const FiniteFloat rainMaxHeight) { w.mRainMaxHeight = rainMaxHeight; });
weatherT["rainMinHeight"] = sol::property([](const MWWorld::Weather& w) { return w.mRainMinHeight; },
[](MWWorld::Weather& w, const FiniteFloat rainMinHeight) { w.mRainMinHeight = rainMinHeight; });
weatherT["rainLoopSoundID"]
= sol::property([](const MWWorld::Weather& w) { return LuaUtil::serializeRefId(w.mRainLoopSoundID); },
[](MWWorld::Weather& w, sol::optional<std::string_view> rainLoopSoundID) {
w.mRainLoopSoundID = ESM::RefId::deserializeText(rainLoopSoundID.value_or(""));
});
weatherT["sunDiscSunsetColor"]
= sol::readonly_property([](const MWWorld::Weather& w) { return color(w.mSunDiscSunsetColor); });
= sol::property([](const MWWorld::Weather& w) { return Misc::Color::fromVec(w.mSunDiscSunsetColor); },
[](MWWorld::Weather& w, const Misc::Color& sunDiscSunsetColor) {
w.mSunDiscSunsetColor = sunDiscSunsetColor.toVec();
});
weatherT["ambientLoopSoundID"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mAmbientLoopSoundID.serializeText(); });
weatherT["ambientColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) {
sol::table result(lua, sol::create);
result["sunrise"] = color(w.mAmbientColor.getSunriseValue());
result["day"] = color(w.mAmbientColor.getDayValue());
result["sunset"] = color(w.mAmbientColor.getSunsetValue());
result["night"] = color(w.mAmbientColor.getNightValue());
return result;
});
weatherT["fogColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) {
sol::table result(lua, sol::create);
result["sunrise"] = color(w.mFogColor.getSunriseValue());
result["day"] = color(w.mFogColor.getDayValue());
result["sunset"] = color(w.mFogColor.getSunsetValue());
result["night"] = color(w.mFogColor.getNightValue());
return result;
});
weatherT["skyColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) {
sol::table result(lua, sol::create);
result["sunrise"] = color(w.mSkyColor.getSunriseValue());
result["day"] = color(w.mSkyColor.getDayValue());
result["sunset"] = color(w.mSkyColor.getSunsetValue());
result["night"] = color(w.mSkyColor.getNightValue());
return result;
});
weatherT["sunColor"] = sol::readonly_property([lua](const MWWorld::Weather& w) {
sol::table result(lua, sol::create);
result["sunrise"] = color(w.mSunColor.getSunriseValue());
result["day"] = color(w.mSunColor.getDayValue());
result["sunset"] = color(w.mSunColor.getSunsetValue());
result["night"] = color(w.mSunColor.getNightValue());
return result;
});
weatherT["landFogDepth"] = sol::readonly_property([lua](const MWWorld::Weather& w) {
sol::table result(lua, sol::create);
result["sunrise"] = w.mLandFogDepth.getSunriseValue();
result["day"] = w.mLandFogDepth.getDayValue();
result["sunset"] = w.mLandFogDepth.getSunsetValue();
result["night"] = w.mLandFogDepth.getNightValue();
return result;
});
weatherT["particleEffect"]
= sol::readonly_property([](const MWWorld::Weather& w) -> sol::optional<std::string> {
if (w.mParticleEffect.empty())
return sol::nullopt;
return w.mParticleEffect;
});
weatherT["distantLandFogFactor"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mDL.FogFactor; });
weatherT["distantLandFogOffset"]
= sol::readonly_property([](const MWWorld::Weather& w) { return w.mDL.FogOffset; });
weatherT["scriptId"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mScriptId; });
weatherT["recordId"] = sol::readonly_property([](const MWWorld::Weather& w) { return w.mId.serializeText(); });
= sol::property([](const MWWorld::Weather& w) { return LuaUtil::serializeRefId(w.mAmbientLoopSoundID); },
[](MWWorld::Weather& w, sol::optional<std::string_view> ambientLoopSoundId) {
w.mAmbientLoopSoundID = ESM::RefId::deserializeText(ambientLoopSoundId.value_or(""));
});
weatherT["ambientColor"] = sol::readonly_property([](const MWWorld::Weather& w) { return &w.mAmbientColor; });
weatherT["fogColor"] = sol::readonly_property([](const MWWorld::Weather& w) { return &w.mFogColor; });
weatherT["skyColor"] = sol::readonly_property([](const MWWorld::Weather& w) { return &w.mSkyColor; });
weatherT["sunColor"] = sol::readonly_property([](const MWWorld::Weather& w) { return &w.mSunColor; });
weatherT["landFogDepth"] = sol::readonly_property([](const MWWorld::Weather& w) { return &w.mLandFogDepth; });
weatherT["particleEffect"] = sol::property(
[](const MWWorld::Weather& w) -> sol::optional<std::string> {
if (w.mParticleEffect.empty())
return sol::nullopt;
return w.mParticleEffect;
},
[](MWWorld::Weather& w, sol::optional<std::string_view> particleEffect) {
w.mParticleEffect = particleEffect.value_or("");
});
weatherT["distantLandFogFactor"] = sol::property([](const MWWorld::Weather& w) { return w.mDL.FogFactor; },
[](MWWorld::Weather& w, const FiniteFloat fogFactor) { w.mDL.FogFactor = fogFactor; });
weatherT["distantLandFogOffset"] = sol::property([](const MWWorld::Weather& w) { return w.mDL.FogOffset; },
[](MWWorld::Weather& w, const FiniteFloat fogOffset) { w.mDL.FogOffset = fogOffset; });
api["changeWeather"] = [](std::string_view regionId, const MWWorld::Weather& weather) {
ESM::RefId region = ESM::RefId::deserializeText(regionId);

View File

@ -10,7 +10,7 @@
#include <components/esm3/loadnpc.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/lua/luastate.hpp>
#include <components/misc/finitenumbers.hpp>
#include <components/misc/finitevalues.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"

View File

@ -114,6 +114,11 @@ namespace MWWorld
const T& getSunsetValue() const { return mSunsetValue; }
const T& getNightValue() const { return mNightValue; }
void setSunriseValue(const T& sunriseValue) { mSunriseValue = sunriseValue; }
void setDayValue(const T& dayValue) { mDayValue = dayValue; }
void setSunsetValue(const T& sunsetValue) { mSunsetValue = sunsetValue; }
void setNightValue(const T& nightValue) { mNightValue = nightValue; }
private:
T mSunriseValue, mDayValue, mSunsetValue, mNightValue;
};

View File

@ -45,6 +45,11 @@ namespace Misc
return Color(SceneUtil::colourFromRGB(value));
}
Color Color::fromVec(osg::Vec4f value)
{
return Color(std::move(value));
}
std::string Color::toHex() const
{
std::string result(6, '0');

View File

@ -27,9 +27,11 @@ namespace Misc
static Color fromHex(std::string_view hex);
static Color fromRGB(unsigned int value);
static Color fromVec(osg::Vec4f value);
std::string toHex() const;
unsigned int toRGBA() const { return mValue.asRGBA(); }
osg::Vec4f toVec() const { return mValue; }
friend bool operator==(const Color& l, const Color& r);

View File

@ -1,53 +0,0 @@
#ifndef OPENMW_COMPONENTS_MISC_FINITENUMBERS_HPP
#define OPENMW_COMPONENTS_MISC_FINITENUMBERS_HPP
#include <components/lua/luastate.hpp>
#include <cmath>
#include <stdexcept>
#include <utility>
namespace Misc
{
template <class T>
struct FiniteNumber
{
T mValue;
FiniteNumber(T v)
{
if (!std::isfinite(v))
throw std::invalid_argument("Value must be a finite number");
mValue = v;
}
operator T() const { return mValue; }
};
using FiniteDouble = FiniteNumber<double>;
using FiniteFloat = FiniteNumber<float>;
}
namespace sol
{
template <class Handler, class T>
bool sol_lua_check(
types<Misc::FiniteNumber<T>>, lua_State* state, int index, Handler&& handler, stack::record& tracking)
{
bool success = stack::check<T>(state, lua_absindex(state, index), std::forward<Handler>(handler));
tracking.use(1);
return success;
}
template <class T>
static Misc::FiniteNumber<T> sol_lua_get(
types<Misc::FiniteNumber<T>>, lua_State* state, int index, stack::record& tracking)
{
T value = stack::get<T>(state, lua_absindex(state, index));
tracking.use(1);
return value;
}
}
#endif

View File

@ -0,0 +1,76 @@
#ifndef OPENMW_COMPONENTS_MISC_FINITEVALUES_HPP
#define OPENMW_COMPONENTS_MISC_FINITEVALUES_HPP
#include <osg/Vec2f>
#include <osg/Vec3f>
#include <osg/Vec4f>
#include <components/lua/luastate.hpp>
#include <cmath>
#include <stdexcept>
#include <type_traits>
#include <utility>
namespace Misc
{
template <class T>
struct FiniteValue
{
T mValue;
FiniteValue(T v)
{
constexpr bool isVecType
= std::is_same_v<osg::Vec2f, T> || std::is_same_v<osg::Vec3f, T> || std::is_same_v<osg::Vec4f, T>;
if constexpr (isVecType)
{
for (int i = 0; i < T::num_components; ++i)
if (!std::isfinite(v[i]))
throw std::invalid_argument("Vector must contain finite numbers");
}
else
{
if (!std::isfinite(v))
throw std::invalid_argument("Value must be a finite number");
}
mValue = v;
}
operator T() const { return mValue; }
};
using FiniteDouble = FiniteValue<double>;
using FiniteFloat = FiniteValue<float>;
using FiniteVec2f = FiniteValue<osg::Vec2f>;
using FiniteVec3f = FiniteValue<osg::Vec3f>;
using FiniteVec4f = FiniteValue<osg::Vec4f>;
}
namespace sol
{
template <class Handler, class T>
bool sol_lua_check(
types<Misc::FiniteValue<T>>, lua_State* state, int index, Handler&& handler, stack::record& tracking)
{
bool success = stack::check<T>(state, lua_absindex(state, index), std::forward<Handler>(handler));
tracking.use(1);
return success;
}
template <class T>
static Misc::FiniteValue<T> sol_lua_get(
types<Misc::FiniteValue<T>>, lua_State* state, int index, stack::record& tracking)
{
T value = stack::get<T>(state, lua_absindex(state, index));
tracking.use(1);
return value;
}
}
#endif

View File

@ -1304,7 +1304,6 @@
---
-- Weather data
-- @type WeatherRecord
-- @extends #userdata
-- @field #string recordId
-- @field #number scriptId
-- @field #string name
@ -1329,10 +1328,26 @@
-- @field #number distantLandFogFactor
-- @field #number distantLandFogOffset
-- @field openmw.util#Color sunDiscSunsetColor
-- @field #table landFogDepth A table with the keys "sunrise", "day", "sunset" and "night"
-- @field #table skyColor A table with the keys "sunrise", "day", "sunset" and "night". Each is a @{openmw.util#Color}.
-- @field #table ambientColor A table with the keys "sunrise", "day", "sunset" and "night". Each is a @{openmw.util#Color}.
-- @field #table fogColor A table with the keys "sunrise", "day", "sunset" and "night". Each is a @{openmw.util#Color}.
-- @field #table sunColor A table with the keys "sunrise", "day", "sunset" and "night". Each is a @{openmw.util#Color}.
-- @field #TimeOfDayInterpolatorFloat landFogDepth
-- @field #TimeOfDayInterpolatorColor skyColor
-- @field #TimeOfDayInterpolatorColor ambientColor
-- @field #TimeOfDayInterpolatorColor fogColor
-- @field #TimeOfDayInterpolatorColor sunColor
---
-- Interpolates numbers for weathers based on time of day
-- @type TimeOfDayInterpolatorFloat
-- @field #number sunrise
-- @field #number sunset
-- @field #number day
-- @field #number night
---
-- Interpolates colors for weathers based on time of day
-- @type TimeOfDayInterpolatorColor
-- @field openmw.util#Color sunrise
-- @field openmw.util#Color sunset
-- @field openmw.util#Color day
-- @field openmw.util#Color night
return nil

View File

@ -351,13 +351,20 @@ testing.registerGlobalTest('load while teleporting - teleport', function()
landracer:teleport(player.cell, player.position)
end)
testing.registerGlobalTest('nan', function()
testing.registerGlobalTest('nan float', function()
local nan = 0.0 / 0.0
local ok, err = pcall(function() world.setGameTimeScale(nan) end)
testing.expectEqual(ok, false)
testing.expectEqual(err, 'Value must be a finite number')
end)
testing.registerGlobalTest('nan vector', function()
local nan = 0.0 / 0.0
local ok, err = pcall(function() core.weather.records[1].stormDirection = util.vector3(nan, nan, nan) end)
testing.expectEqual(ok, false)
testing.expectEqual(err, 'Vector must contain finite numbers')
end)
testing.registerGlobalTest('mwscript magic interactions', function()
local player = world.players[1]
local script = world.mwscript.getGlobalScript('OpenMW_Tests', player)

View File

@ -73,7 +73,8 @@ registerGlobalTest('memory limit')
registerGlobalTest('vfs')
registerGlobalTest('commit crime')
registerGlobalTest('record model property')
registerGlobalTest('nan', 'world.setGameTimeScale should not accept nan')
registerGlobalTest('nan float', 'world.setGameTimeScale should not accept nan')
registerGlobalTest('nan vector', 'weather.stormDirection should not accept a vector with a nan component')
registerGlobalTest('player yaw rotation', 'rotating player with controls.yawChange should change rotation')
registerGlobalTest('player pitch rotation', 'rotating player with controls.pitchChange should change rotation')

View File

@ -12,6 +12,7 @@ require('global_issues')
require('global_dialogues')
require('global_mwscript')
require('global_regions')
require('global_weather')
return {
engineHandlers = {

View File

@ -0,0 +1,10 @@
local testing = require('testing_util')
local core = require('openmw.core')
local util = require('openmw.util')
testing.registerGlobalTest('[weather] Should be able to change weather records', function()
local record = core.weather.records['clear']
testing.expect(record ~= nil, 'Clear weather to exist')
record.skyColor.sunrise = util.color.hex('2227FF')
testing.expectEqual(record.skyColor.sunrise, util.color.hex('2227FF'))
end)