Merge branch 'master' of https://gitlab.com/OpenMW/openmw.git into fix/osg-animation-rename-update-order-sucks-this-took-too-long

pull/3236/head
Sam Hellawell 6 months ago
commit df0a7a849b

@ -184,6 +184,8 @@
Bug #8005: F3 stats bars are sorted not according to their place in the timeline
Bug #8018: Potion effects should never explode and always apply on self
Bug #8021: Player's scale doesn't reset when starting a new game
Bug #8048: Actors can generate negative collision extents and have no collision
Bug #8064: Lua move360 script doesn't respect the enableZoom/disableZoom Camera interface setting
Feature #1415: Infinite fall failsafe
Feature #2566: Handle NAM9 records for manual cell references
Feature #3537: Shader-based water ripples
@ -249,6 +251,7 @@
Feature #7971: Make save's Time Played value display hours instead of days
Feature #7985: Support dark mode on Windows
Feature #8034: (Lua) Containers should have respawning/organic flags
Feature #8067: Support Game Mode on macOS
Task #5896: Do not use deprecated MyGUI properties
Task #6085: Replace boost::filesystem with std::filesystem
Task #6149: Dehardcode Lua API_REVISION

@ -82,7 +82,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 62)
set(OPENMW_LUA_API_REVISION 64)
set(OPENMW_POSTPROCESSING_API_REVISION 1)
set(OPENMW_VERSION_COMMITHASH "")

@ -1364,4 +1364,28 @@ namespace
EXPECT_EQ(*result, expected);
}
TEST_F(TestBulletNifLoader, dont_assign_invalid_bounding_box_extents)
{
copy(mTransform, mNiTriShape.mTransform);
mNiTriShape.mTransform.mScale = 10;
mNiTriShape.mParents.push_back(&mNiNode);
mNiTriShape2.mName = "Bounding Box";
mNiTriShape2.mBounds.mType = Nif::BoundingVolume::Type::BOX_BV;
mNiTriShape2.mBounds.mBox.mExtents = osg::Vec3f(-1, -2, -3);
mNiTriShape2.mParents.push_back(&mNiNode);
mNiNode.mChildren = Nif::NiAVObjectList{ Nif::NiAVObjectPtr(&mNiTriShape), Nif::NiAVObjectPtr(&mNiTriShape2) };
Nif::NIFFile file("test.nif");
file.mRoots.push_back(&mNiNode);
const auto result = mLoader.load(file);
const bool extentsUnassigned
= std::ranges::all_of(result->mCollisionBox.mExtents._v, [](float extent) { return extent == 0.f; });
EXPECT_EQ(extentsUnassigned, true);
}
}

@ -319,7 +319,6 @@ bool Launcher::SettingsPage::loadSettings()
// Miscellaneous
{
// Saves
loadSettingBool(Settings::saves().mTimeplayed, *timePlayedCheckbox);
loadSettingInt(Settings::saves().mMaxQuicksaves, *maximumQuicksavesComboBox);
// Other Settings
@ -512,7 +511,6 @@ void Launcher::SettingsPage::saveSettings()
// Miscellaneous
{
// Saves Settings
saveSettingBool(*timePlayedCheckbox, Settings::saves().mTimeplayed);
saveSettingInt(*maximumQuicksavesComboBox, Settings::saves().mMaxQuicksaves);
// Other Settings

@ -1430,16 +1430,6 @@
<string>Saves</string>
</property>
<layout class="QVBoxLayout" name="savesGroupVerticalLayout">
<item>
<widget class="QCheckBox" name="timePlayedCheckbox">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add &quot;Time Played&quot; to Saves</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="maximumQuicksavesLayout">
<item>

@ -71,7 +71,7 @@ bool CSVWorld::DataDisplayDelegate::eventFilter(QObject* target, QEvent* event)
QColor themeColor = QApplication::palette().text().color();
if (themeColor != mPixmapsColor)
{
mPixmapsColor = themeColor;
mPixmapsColor = std::move(themeColor);
buildPixmaps();
}

@ -212,7 +212,7 @@ void CSVWorld::ScriptSubView::useHint(const std::string& hint)
if (hint.empty())
return;
unsigned line = 0, column = 0;
int line = 0, column = 0;
char c;
std::istringstream stream(hint.c_str() + 1);
switch (hint[0])
@ -222,8 +222,8 @@ void CSVWorld::ScriptSubView::useHint(const std::string& hint)
{
QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn);
QString source = mModel->data(index).toString();
unsigned stringSize = source.length();
unsigned pos, dummy;
int stringSize = static_cast<int>(source.length());
int pos, dummy;
if (!(stream >> c >> dummy >> pos))
return;
@ -234,7 +234,7 @@ void CSVWorld::ScriptSubView::useHint(const std::string& hint)
pos = stringSize;
}
for (unsigned i = 0; i <= pos; ++i)
for (int i = 0; i <= pos; ++i)
{
if (source[i] == '\n')
{

@ -245,7 +245,7 @@ bool OMW::Engine::frame(unsigned frameNumber, float frametime)
if (mStateManager->getState() != MWBase::StateManager::State_NoGame)
{
if (!mWindowManager->containsMode(MWGui::GM_MainMenu))
if (!mWindowManager->containsMode(MWGui::GM_MainMenu) || !paused)
{
if (mWorld->getScriptsEnabled())
{

@ -435,7 +435,7 @@ namespace MWGui
mCurrentSlot->mProfile.mInGameTime.mMonth)
<< " " << hour << " " << (pm ? "#{Calendar:pm}" : "#{Calendar:am}");
if (Settings::saves().mTimeplayed)
if (mCurrentSlot->mProfile.mTimePlayed > 0)
{
text << "\n"
<< "#{OMWEngine:TimePlayed}: " << formatTimeplayed(mCurrentSlot->mProfile.mTimePlayed);

@ -1,22 +1,28 @@
#include "itemdata.hpp"
#include <components/esm3/loadcrea.hpp>
#include <components/esm3/loadench.hpp>
#include "context.hpp"
#include "luamanagerimp.hpp"
#include "objectvariant.hpp"
#include "../mwbase/environment.hpp"
#include "../mwmechanics/spellutil.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
namespace
{
using SelfObject = MWLua::SelfObject;
using Index = const SelfObject::CachedStat::Index&;
constexpr std::array properties = { "condition", /*"enchantmentCharge", "soul", "owner", etc..*/ };
constexpr std::array properties = { "condition", "enchantmentCharge", "soul" };
void invalidPropErr(std::string_view prop, const MWWorld::Ptr& ptr)
void valueErr(std::string_view prop, std::string type)
{
throw std::runtime_error("'" + std::string(prop) + "'" + " property does not exist for item "
+ std::string(ptr.getClass().getName(ptr)) + "(" + std::string(ptr.getTypeDescription()) + ")");
throw std::logic_error("'" + std::string(prop) + "'" + " received invalid value type (" + type + ")");
}
}
@ -54,26 +60,56 @@ namespace MWLua
if (it != self->mStatsCache.end())
return it->second;
}
return sol::make_object(context.mLua->sol(), getValue(context, prop));
return sol::make_object(context.mLua->sol(), getValue(context, prop, mObject.ptr()));
}
void set(const Context& context, std::string_view prop, const sol::object& value) const
{
SelfObject* obj = mObject.asSelfObject();
addStatUpdateAction(context.mLuaManager, *obj);
obj->mStatsCache[SelfObject::CachedStat{ &ItemData::setValue, std::monostate{}, prop }] = value;
if (mObject.isGObject())
setValue({}, prop, mObject.ptr(), value);
else if (mObject.isSelfObject())
{
SelfObject* obj = mObject.asSelfObject();
addStatUpdateAction(context.mLuaManager, *obj);
obj->mStatsCache[SelfObject::CachedStat{ &ItemData::setValue, std::monostate{}, prop }] = value;
}
else
throw std::runtime_error("Only global or self scripts can set the value");
}
sol::object getValue(const Context& context, std::string_view prop) const
static sol::object getValue(const Context& context, std::string_view prop, const MWWorld::Ptr& ptr)
{
if (prop == "condition")
{
MWWorld::Ptr o = mObject.ptr();
if (o.mRef->getType() == ESM::REC_LIGH)
return sol::make_object(context.mLua->sol(), o.getClass().getRemainingUsageTime(o));
else if (o.getClass().hasItemHealth(o))
return sol::make_object(
context.mLua->sol(), o.getClass().getItemHealth(o) + o.getCellRef().getChargeIntRemainder());
if (ptr.mRef->getType() == ESM::REC_LIGH)
return sol::make_object(context.mLua->sol(), ptr.getClass().getRemainingUsageTime(ptr));
else if (ptr.getClass().hasItemHealth(ptr))
return sol::make_object(context.mLua->sol(),
ptr.getClass().getItemHealth(ptr) + ptr.getCellRef().getChargeIntRemainder());
}
else if (prop == "enchantmentCharge")
{
const ESM::RefId& enchantmentName = ptr.getClass().getEnchantment(ptr);
if (enchantmentName.empty())
return sol::lua_nil;
float charge = ptr.getCellRef().getEnchantmentCharge();
const auto& store = MWBase::Environment::get().getESMStore();
const auto* enchantment = store->get<ESM::Enchantment>().find(enchantmentName);
if (charge == -1) // return the full charge
return sol::make_object(context.mLua->sol(), MWMechanics::getEnchantmentCharge(*enchantment));
return sol::make_object(context.mLua->sol(), charge);
}
else if (prop == "soul")
{
ESM::RefId soul = ptr.getCellRef().getSoul();
if (soul.empty())
return sol::lua_nil;
return sol::make_object(context.mLua->sol(), soul.serializeText());
}
return sol::lua_nil;
@ -83,17 +119,48 @@ namespace MWLua
{
if (prop == "condition")
{
float cond = LuaUtil::cast<float>(value);
if (ptr.mRef->getType() == ESM::REC_LIGH)
ptr.getClass().setRemainingUsageTime(ptr, cond);
else if (ptr.getClass().hasItemHealth(ptr))
if (value.get_type() == sol::type::number)
{
float cond = LuaUtil::cast<float>(value);
if (ptr.mRef->getType() == ESM::REC_LIGH)
ptr.getClass().setRemainingUsageTime(ptr, cond);
else if (ptr.getClass().hasItemHealth(ptr))
{
// if the value set is less than 0, chargeInt and chargeIntRemainder is set to 0
ptr.getCellRef().setChargeIntRemainder(std::max(0.f, std::modf(cond, &cond)));
ptr.getCellRef().setCharge(std::max(0.f, cond));
}
}
else
valueErr(prop, sol::type_name(value.lua_state(), value.get_type()));
}
else if (prop == "enchantmentCharge")
{
if (value.get_type() == sol::type::lua_nil)
ptr.getCellRef().setEnchantmentCharge(-1);
else if (value.get_type() == sol::type::number)
ptr.getCellRef().setEnchantmentCharge(std::max(0.0f, LuaUtil::cast<float>(value)));
else
valueErr(prop, sol::type_name(value.lua_state(), value.get_type()));
}
else if (prop == "soul")
{
if (value.get_type() == sol::type::lua_nil)
ptr.getCellRef().setSoul(ESM::RefId{});
else if (value.get_type() == sol::type::string)
{
// if the value set is less than 0, chargeInt and chargeIntRemainder is set to 0
ptr.getCellRef().setChargeIntRemainder(std::max(0.f, std::modf(cond, &cond)));
ptr.getCellRef().setCharge(std::max(0.f, cond));
std::string_view souldId = LuaUtil::cast<std::string_view>(value);
ESM::RefId creature = ESM::RefId::deserializeText(souldId);
const auto& store = *MWBase::Environment::get().getESMStore();
// TODO: Add Support for NPC Souls
if (store.get<ESM::Creature>().search(creature))
ptr.getCellRef().setSoul(creature);
else
throw std::runtime_error("Cannot use non-existent creature as a soul: " + std::string(souldId));
}
else
invalidPropErr(prop, ptr);
valueErr(prop, sol::type_name(value.lua_state(), value.get_type()));
}
}
};

@ -1,6 +1,7 @@
#include "localscripts.hpp"
#include <components/esm3/loadcell.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/misc/strings/lower.hpp>
#include "../mwbase/environment.hpp"
@ -13,6 +14,7 @@
#include "../mwmechanics/aisequence.hpp"
#include "../mwmechanics/aitravel.hpp"
#include "../mwmechanics/aiwander.hpp"
#include "../mwmechanics/attacktype.hpp"
#include "../mwmechanics/creaturestats.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/ptr.hpp"
@ -63,6 +65,11 @@ namespace MWLua
selfAPI["controls"] = sol::readonly_property([](SelfObject& self) { return &self.mControls; });
selfAPI["isActive"] = [](SelfObject& self) { return &self.mIsActive; };
selfAPI["enableAI"] = [](SelfObject& self, bool v) { self.mControls.mDisableAI = !v; };
selfAPI["ATTACK_TYPE"]
= LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs<std::string_view, MWMechanics::AttackType>(
{ { "NoAttack", MWMechanics::AttackType::NoAttack }, { "Any", MWMechanics::AttackType::Any },
{ "Chop", MWMechanics::AttackType::Chop }, { "Slash", MWMechanics::AttackType::Slash },
{ "Thrust", MWMechanics::AttackType::Thrust } }));
using AiPackage = MWMechanics::AiPackage;
sol::usertype<AiPackage> aiPackage = context.mLua->sol().new_usertype<AiPackage>("AiPackage");

@ -2,6 +2,7 @@
#include <components/esm3/loadacti.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -51,7 +52,8 @@ namespace MWLua
record["model"] = sol::readonly_property([](const ESM::Activator& rec) -> std::string {
return Misc::ResourceHelpers::correctMeshPath(rec.mModel);
});
record["mwscript"] = sol::readonly_property(
[](const ESM::Activator& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property([](const ESM::Activator& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
}
}

@ -2,6 +2,7 @@
#include <components/esm3/loadappa.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -39,8 +40,9 @@ namespace MWLua
record["model"] = sol::readonly_property([](const ESM::Apparatus& rec) -> std::string {
return Misc::ResourceHelpers::correctMeshPath(rec.mModel);
});
record["mwscript"] = sol::readonly_property(
[](const ESM::Apparatus& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property([](const ESM::Apparatus& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
record["icon"] = sol::readonly_property([vfs](const ESM::Apparatus& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});

@ -2,6 +2,7 @@
#include <components/esm3/loadarmo.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -98,10 +99,10 @@ namespace MWLua
record["icon"] = sol::readonly_property([vfs](const ESM::Armor& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});
record["enchant"]
= sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mEnchant.serializeText(); });
record["mwscript"]
= sol::readonly_property([](const ESM::Armor& rec) -> std::string { return rec.mScript.serializeText(); });
record["enchant"] = sol::readonly_property(
[](const ESM::Armor& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mEnchant); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Armor& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["weight"] = sol::readonly_property([](const ESM::Armor& rec) -> float { return rec.mData.mWeight; });
record["value"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mValue; });
record["type"] = sol::readonly_property([](const ESM::Armor& rec) -> int { return rec.mData.mType; });

@ -6,6 +6,7 @@
#include <components/esm3/loadbook.hpp>
#include <components/esm3/loadskil.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -104,14 +105,14 @@ namespace MWLua
record["name"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mName; });
record["model"] = sol::readonly_property(
[](const ESM::Book& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); });
record["mwscript"]
= sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Book& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["icon"] = sol::readonly_property([vfs](const ESM::Book& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});
record["text"] = sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mText; });
record["enchant"]
= sol::readonly_property([](const ESM::Book& rec) -> std::string { return rec.mEnchant.serializeText(); });
record["enchant"] = sol::readonly_property(
[](const ESM::Book& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mEnchant); });
record["isScroll"] = sol::readonly_property([](const ESM::Book& rec) -> bool { return rec.mData.mIsScroll; });
record["value"] = sol::readonly_property([](const ESM::Book& rec) -> int { return rec.mData.mValue; });
record["weight"] = sol::readonly_property([](const ESM::Book& rec) -> float { return rec.mData.mWeight; });
@ -119,9 +120,7 @@ namespace MWLua
= sol::readonly_property([](const ESM::Book& rec) -> float { return rec.mData.mEnchant * 0.1f; });
record["skill"] = sol::readonly_property([](const ESM::Book& rec) -> sol::optional<std::string> {
ESM::RefId skill = ESM::Skill::indexToRefId(rec.mData.mSkillId);
if (!skill.empty())
return skill.serializeText();
return sol::nullopt;
return LuaUtil::serializeRefId(skill);
});
}
}

@ -2,6 +2,7 @@
#include <components/esm3/loadclot.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -93,10 +94,12 @@ namespace MWLua
record["icon"] = sol::readonly_property([vfs](const ESM::Clothing& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});
record["enchant"] = sol::readonly_property(
[](const ESM::Clothing& rec) -> std::string { return rec.mEnchant.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Clothing& rec) -> std::string { return rec.mScript.serializeText(); });
record["enchant"] = sol::readonly_property([](const ESM::Clothing& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mEnchant);
});
record["mwscript"] = sol::readonly_property([](const ESM::Clothing& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
record["weight"] = sol::readonly_property([](const ESM::Clothing& rec) -> float { return rec.mData.mWeight; });
record["value"] = sol::readonly_property([](const ESM::Clothing& rec) -> int { return rec.mData.mValue; });
record["type"] = sol::readonly_property([](const ESM::Clothing& rec) -> int { return rec.mData.mType; });

@ -2,6 +2,7 @@
#include <components/esm3/loadcont.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -51,8 +52,9 @@ namespace MWLua
record["model"] = sol::readonly_property([](const ESM::Container& rec) -> std::string {
return Misc::ResourceHelpers::correctMeshPath(rec.mModel);
});
record["mwscript"] = sol::readonly_property(
[](const ESM::Container& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property([](const ESM::Container& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
record["weight"] = sol::readonly_property([](const ESM::Container& rec) -> float { return rec.mWeight; });
record["isOrganic"] = sol::readonly_property(
[](const ESM::Container& rec) -> bool { return rec.mFlags & ESM::Container::Organic; });

@ -4,6 +4,7 @@
#include <components/esm3/loadcrea.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -36,8 +37,9 @@ namespace MWLua
record["name"] = sol::readonly_property([](const ESM::Creature& rec) -> std::string { return rec.mName; });
record["model"] = sol::readonly_property(
[](const ESM::Creature& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Creature& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property([](const ESM::Creature& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
record["baseCreature"] = sol::readonly_property(
[](const ESM::Creature& rec) -> std::string { return rec.mOriginal.serializeText(); });
record["soulValue"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mData.mSoul; });

@ -2,6 +2,7 @@
#include <components/esm3/loaddoor.hpp>
#include <components/esm4/loaddoor.hpp>
#include <components/lua/util.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/misc/convert.hpp>
#include <components/misc/resourcehelpers.hpp>
@ -64,8 +65,8 @@ namespace MWLua
record["name"] = sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mName; });
record["model"] = sol::readonly_property(
[](const ESM::Door& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); });
record["mwscript"]
= sol::readonly_property([](const ESM::Door& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Door& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["openSound"] = sol::readonly_property(
[](const ESM::Door& rec) -> std::string { return rec.mOpenSound.serializeText(); });
record["closeSound"] = sol::readonly_property(

@ -34,8 +34,9 @@ namespace MWLua
record["model"] = sol::readonly_property([](const ESM::Ingredient& rec) -> std::string {
return Misc::ResourceHelpers::correctMeshPath(rec.mModel);
});
record["mwscript"] = sol::readonly_property(
[](const ESM::Ingredient& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property([](const ESM::Ingredient& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
record["icon"] = sol::readonly_property([vfs](const ESM::Ingredient& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});

@ -11,6 +11,7 @@ namespace MWLua
{
void addItemBindings(sol::table item, const Context& context)
{
// Deprecated. Moved to itemData; should be removed later
item["getEnchantmentCharge"] = [](const Object& object) -> sol::optional<float> {
float charge = object.ptr().getCellRef().getEnchantmentCharge();
if (charge == -1)

@ -2,6 +2,7 @@
#include <components/esm3/loadligh.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -96,8 +97,8 @@ namespace MWLua
});
record["sound"]
= sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mSound.serializeText(); });
record["mwscript"]
= sol::readonly_property([](const ESM::Light& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Light& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["weight"] = sol::readonly_property([](const ESM::Light& rec) -> float { return rec.mData.mWeight; });
record["value"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mValue; });
record["duration"] = sol::readonly_property([](const ESM::Light& rec) -> int { return rec.mData.mTime; });

@ -2,6 +2,7 @@
#include <components/esm3/loadlock.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -31,8 +32,9 @@ namespace MWLua
record["name"] = sol::readonly_property([](const ESM::Lockpick& rec) -> std::string { return rec.mName; });
record["model"] = sol::readonly_property(
[](const ESM::Lockpick& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Lockpick& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property([](const ESM::Lockpick& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
record["icon"] = sol::readonly_property([vfs](const ESM::Lockpick& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});

@ -3,6 +3,7 @@
#include <components/esm3/loadcrea.hpp>
#include <components/esm3/loadmisc.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -55,6 +56,7 @@ namespace MWLua
addRecordFunctionBinding<ESM::Miscellaneous>(miscellaneous, context);
miscellaneous["createRecordDraft"] = tableToMisc;
// Deprecated. Moved to itemData; should be removed later
miscellaneous["setSoul"] = [](const GObject& object, std::string_view soulId) {
ESM::RefId creature = ESM::RefId::deserializeText(soulId);
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
@ -69,12 +71,10 @@ namespace MWLua
};
miscellaneous["getSoul"] = [](const Object& object) -> sol::optional<std::string> {
ESM::RefId soul = object.ptr().getCellRef().getSoul();
if (soul.empty())
return sol::nullopt;
else
return soul.serializeText();
return LuaUtil::serializeRefId(soul);
};
miscellaneous["soul"] = miscellaneous["getSoul"]; // for compatibility; should be removed later
sol::usertype<ESM::Miscellaneous> record
= context.mLua->sol().new_usertype<ESM::Miscellaneous>("ESM3_Miscellaneous");
record[sol::meta_function::to_string]
@ -85,8 +85,9 @@ namespace MWLua
record["model"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> std::string {
return Misc::ResourceHelpers::correctMeshPath(rec.mModel);
});
record["mwscript"] = sol::readonly_property(
[](const ESM::Miscellaneous& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property([](const ESM::Miscellaneous& rec) -> sol::optional<std::string> {
return LuaUtil::serializeRefId(rec.mScript);
});
record["icon"] = sol::readonly_property([vfs](const ESM::Miscellaneous& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});

@ -85,8 +85,8 @@ namespace MWLua
= sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mRace.serializeText(); });
record["class"]
= sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mClass.serializeText(); });
record["mwscript"]
= sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::NPC& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["hair"]
= sol::readonly_property([](const ESM::NPC& rec) -> std::string { return rec.mHair.serializeText(); });
record["baseDisposition"]

@ -80,8 +80,8 @@ namespace MWLua
record["icon"] = sol::readonly_property([vfs](const ESM::Potion& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});
record["mwscript"]
= sol::readonly_property([](const ESM::Potion& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Potion& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["weight"] = sol::readonly_property([](const ESM::Potion& rec) -> float { return rec.mData.mWeight; });
record["value"] = sol::readonly_property([](const ESM::Potion& rec) -> int { return rec.mData.mValue; });
record["effects"] = sol::readonly_property([context](const ESM::Potion& rec) -> sol::table {

@ -2,6 +2,7 @@
#include <components/esm3/loadprob.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -31,8 +32,8 @@ namespace MWLua
record["name"] = sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mName; });
record["model"] = sol::readonly_property(
[](const ESM::Probe& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); });
record["mwscript"]
= sol::readonly_property([](const ESM::Probe& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Probe& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["icon"] = sol::readonly_property([vfs](const ESM::Probe& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});

@ -2,6 +2,7 @@
#include <components/esm3/loadrepa.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -31,8 +32,8 @@ namespace MWLua
record["name"] = sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mName; });
record["model"] = sol::readonly_property(
[](const ESM::Repair& rec) -> std::string { return Misc::ResourceHelpers::correctMeshPath(rec.mModel); });
record["mwscript"]
= sol::readonly_property([](const ESM::Repair& rec) -> std::string { return rec.mScript.serializeText(); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Repair& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["icon"] = sol::readonly_property([vfs](const ESM::Repair& rec) -> std::string {
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});

@ -2,6 +2,7 @@
#include <components/esm3/loadweap.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/util.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
@ -132,9 +133,9 @@ namespace MWLua
return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs);
});
record["enchant"] = sol::readonly_property(
[](const ESM::Weapon& rec) -> std::string { return rec.mEnchant.serializeText(); });
record["mwscript"]
= sol::readonly_property([](const ESM::Weapon& rec) -> std::string { return rec.mScript.serializeText(); });
[](const ESM::Weapon& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mEnchant); });
record["mwscript"] = sol::readonly_property(
[](const ESM::Weapon& rec) -> sol::optional<std::string> { return LuaUtil::serializeRefId(rec.mScript); });
record["isMagical"] = sol::readonly_property(
[](const ESM::Weapon& rec) -> bool { return rec.mData.mFlags & ESM::Weapon::Magical; });
record["isSilver"] = sol::readonly_property(

@ -47,6 +47,7 @@
#include "aifollow.hpp"
#include "aipursue.hpp"
#include "aiwander.hpp"
#include "attacktype.hpp"
#include "character.hpp"
#include "creaturestats.hpp"
#include "movement.hpp"
@ -239,6 +240,23 @@ namespace MWMechanics
namespace
{
std::string_view attackTypeName(AttackType attackType)
{
switch (attackType)
{
case AttackType::NoAttack:
case AttackType::Any:
return {};
case AttackType::Chop:
return "chop";
case AttackType::Slash:
return "slash";
case AttackType::Thrust:
return "thrust";
}
throw std::logic_error("Invalid attack type value: " + std::to_string(static_cast<int>(attackType)));
}
float getTimeToDestination(const AiPackage& package, const osg::Vec3f& position, float speed, float duration,
const osg::Vec3f& halfExtents)
{
@ -363,7 +381,11 @@ namespace MWMechanics
mov.mSpeedFactor = osg::Vec2(controls.mMovement, controls.mSideMovement).length();
stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Run, controls.mRun);
stats.setMovementFlag(MWMechanics::CreatureStats::Flag_Sneak, controls.mSneak);
stats.setAttackingOrSpell((controls.mUse & 1) == 1);
AttackType attackType = static_cast<AttackType>(controls.mUse);
stats.setAttackingOrSpell(attackType != AttackType::NoAttack);
stats.setAttackType(attackTypeName(attackType));
controls.mChanged = false;
}
// For the player we don't need to copy these values to Lua because mwinput doesn't change them.

@ -455,27 +455,37 @@ namespace MWMechanics
void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration,
MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage)
{
switch (storage.mState)
// Attempt to fast forward to the next state instead of remaining in an intermediate state for a frame
for (int i = 0; i < 2; ++i)
{
case AiWanderStorage::Wander_IdleNow:
onIdleStatePerFrameActions(actor, duration, storage);
break;
case AiWanderStorage::Wander_Walking:
onWalkingStatePerFrameActions(actor, duration, supportedMovementDirections, storage);
break;
case AiWanderStorage::Wander_ChooseAction:
onChooseActionStatePerFrameActions(actor, storage);
break;
switch (storage.mState)
{
case AiWanderStorage::Wander_IdleNow:
{
onIdleStatePerFrameActions(actor, duration, storage);
if (storage.mState != AiWanderStorage::Wander_ChooseAction)
return;
continue;
}
case AiWanderStorage::Wander_Walking:
onWalkingStatePerFrameActions(actor, duration, supportedMovementDirections, storage);
return;
case AiWanderStorage::Wander_MoveNow:
break; // nothing to do
case AiWanderStorage::Wander_ChooseAction:
{
onChooseActionStatePerFrameActions(actor, storage);
if (storage.mState != AiWanderStorage::Wander_IdleNow)
return;
continue;
}
case AiWanderStorage::Wander_MoveNow:
return; // nothing to do
default:
// should never get here
assert(false);
break;
default:
// should never get here
assert(false);
return;
}
}
}

@ -0,0 +1,16 @@
#ifndef OPENMW_MWMECHANICS_ATTACKTYPE_H
#define OPENMW_MWMECHANICS_ATTACKTYPE_H
namespace MWMechanics
{
enum class AttackType
{
NoAttack,
Any,
Chop,
Slash,
Thrust
};
}
#endif

@ -1674,7 +1674,12 @@ namespace MWMechanics
}
}
else if (aiInactive)
mAttackType = getRandomAttackType();
{
mAttackType = getDesiredAttackType();
if (mAttackType == "")
mAttackType = getRandomAttackType();
}
// else if (mPtr != getPlayer()) use mAttackType set by AiCombat
startKey = mAttackType + ' ' + startKey;
stopKey = mAttackType + " max attack";
@ -3003,6 +3008,11 @@ namespace MWMechanics
return mPtr.getClass().getCreatureStats(mPtr).getAttackingOrSpell();
}
std::string_view CharacterController::getDesiredAttackType() const
{
return mPtr.getClass().getCreatureStats(mPtr).getAttackType();
}
void CharacterController::setActive(int active) const
{
mAnimation->setActive(active);

@ -247,6 +247,8 @@ namespace MWMechanics
bool getAttackingOrSpell() const;
void setAttackingOrSpell(bool attackingOrSpell) const;
std::string_view getDesiredAttackType() const;
void prepareHit();
public:

@ -97,6 +97,7 @@ namespace MWMechanics
protected:
int mLevel;
bool mAttackingOrSpell;
std::string mAttackType;
public:
CreatureStats();
@ -130,6 +131,7 @@ namespace MWMechanics
const MagicEffects& getMagicEffects() const;
bool getAttackingOrSpell() const { return mAttackingOrSpell; }
std::string_view getAttackType() const { return mAttackType; }
int getLevel() const;
@ -156,6 +158,8 @@ namespace MWMechanics
void setAttackingOrSpell(bool attackingOrSpell) { mAttackingOrSpell = attackingOrSpell; }
void setAttackType(std::string_view attackType) { mAttackType = attackType; }
void setLevel(int level);
void setAiSetting(AiSetting index, Stat<int> value);

@ -2,6 +2,8 @@
#include <osg/FrameBufferObject>
#include "postprocessor.hpp"
namespace MWRender
{
void DistortionCallback::drawImplementation(
@ -10,6 +12,11 @@ namespace MWRender
osg::State* state = renderInfo.getState();
size_t frameId = state->getFrameStamp()->getFrameNumber() % 2;
PostProcessor* postProcessor = dynamic_cast<PostProcessor*>(renderInfo.getCurrentCamera()->getUserData());
if (!postProcessor || bin->getStage()->getFrameBufferObject() != postProcessor->getPrimaryFbo(frameId))
return;
mFBO[frameId]->apply(*state);
const osg::Texture* tex

@ -630,6 +630,12 @@ namespace MWWorld
}
void Store<ESM::Cell>::clearDynamic()
{
for (const auto& [_, cell] : mDynamicExt)
mCells.erase(cell->mId);
mDynamicExt.clear();
for (const auto& [_, cell] : mDynamicInt)
mCells.erase(cell->mId);
mDynamicInt.clear();
setUp();
}

@ -473,8 +473,8 @@ namespace MWWorld
mStore.write(writer, progress); // dynamic Store must be written (and read) before Cells, so that
// references to custom made records will be recognized
mWorldModel.write(writer, progress); // the player's cell needs to be loaded before the player
mPlayer->write(writer, progress);
mWorldModel.write(writer, progress);
mGlobalVariables.write(writer, progress);
mWeatherManager->write(writer, progress);
mProjectileManager->write(writer, progress);

@ -101,6 +101,24 @@ namespace MWWorld
return Cell(*cell);
return std::nullopt;
}
CellStore* getOrCreateExterior(const ESM::ExteriorCellLocation& location,
std::map<ESM::ExteriorCellLocation, MWWorld::CellStore*>& exteriors, ESMStore& store,
ESM::ReadersCache& readers, std::unordered_map<ESM::RefId, CellStore>& cells, bool triggerEvent)
{
if (const auto it = exteriors.find(location); it != exteriors.end())
{
assert(it->second != nullptr);
return it->second;
}
auto [cell, created] = createExteriorCell(location, store);
const ESM::RefId id = cell.getId();
CellStore* const cellStore = &emplaceCellStore(id, std::move(cell), store, readers, cells);
exteriors.emplace(location, cellStore);
if (created && triggerEvent)
MWBase::Environment::get().getLuaManager()->exteriorCreated(*cellStore);
return cellStore;
}
}
}
@ -178,23 +196,7 @@ namespace MWWorld
{
CellStore& WorldModel::getExterior(ESM::ExteriorCellLocation location, bool forceLoad) const
{
const auto it = mExteriors.find(location);
CellStore* cellStore = nullptr;
if (it == mExteriors.end())
{
auto [cell, created] = createExteriorCell(location, mStore);
const ESM::RefId id = cell.getId();
cellStore = &emplaceCellStore(id, std::move(cell), mStore, mReaders, mCells);
mExteriors.emplace(location, cellStore);
if (created)
MWBase::Environment::get().getLuaManager()->exteriorCreated(*cellStore);
}
else
{
assert(it->second != nullptr);
cellStore = it->second;
}
CellStore* cellStore = getOrCreateExterior(location, mExteriors, mStore, mReaders, mCells, true);
if (forceLoad && cellStore->getState() != CellStore::State_Loaded)
cellStore->load();
@ -447,17 +449,26 @@ void MWWorld::WorldModel::write(ESM::ESMWriter& writer, Loading::Listener& progr
}
}
struct GetCellStoreCallback : public MWWorld::CellStore::GetCellStoreCallback
struct MWWorld::WorldModel::GetCellStoreCallback : public CellStore::GetCellStoreCallback
{
public:
GetCellStoreCallback(MWWorld::WorldModel& worldModel)
GetCellStoreCallback(WorldModel& worldModel)
: mWorldModel(worldModel)
{
}
MWWorld::WorldModel& mWorldModel;
WorldModel& mWorldModel;
MWWorld::CellStore* getCellStore(const ESM::RefId& cellId) override { return mWorldModel.findCell(cellId); }
CellStore* getCellStore(const ESM::RefId& cellId) override
{
if (const auto* exteriorId = cellId.getIf<ESM::ESM3ExteriorCellRefId>())
{
ESM::ExteriorCellLocation location(exteriorId->getX(), exteriorId->getY(), ESM::Cell::sDefaultWorldspaceId);
return getOrCreateExterior(
location, mWorldModel.mExteriors, mWorldModel.mStore, mWorldModel.mReaders, mWorldModel.mCells, false);
}
return mWorldModel.findCell(cellId);
}
};
bool MWWorld::WorldModel::readRecord(ESM::ESMReader& reader, uint32_t type)
@ -467,7 +478,10 @@ bool MWWorld::WorldModel::readRecord(ESM::ESMReader& reader, uint32_t type)
ESM::CellState state;
state.mId = reader.getCellId();
CellStore* const cellStore = findCell(state.mId);
GetCellStoreCallback callback(*this);
CellStore* const cellStore = callback.getCellStore(state.mId);
if (cellStore == nullptr)
{
Log(Debug::Warning) << "Dropping state for cell " << state.mId << " (cell no longer exists)";
@ -484,8 +498,6 @@ bool MWWorld::WorldModel::readRecord(ESM::ESMReader& reader, uint32_t type)
if (cellStore->getState() != CellStore::State_Loaded)
cellStore->load();
GetCellStoreCallback callback(*this);
cellStore->readReferences(reader, &callback);
return true;

@ -104,6 +104,8 @@ namespace MWWorld
bool readRecord(ESM::ESMReader& reader, uint32_t type);
private:
struct GetCellStoreCallback;
PtrRegistry mPtrRegistry; // defined before mCells because during destruction it should be the last
MWWorld::ESMStore& mStore;

@ -1,5 +1,7 @@
#include "fileparser.hpp"
#include <components/misc/strings/algorithm.hpp>
#include "scanner.hpp"
#include "tokenloc.hpp"
@ -12,11 +14,6 @@ namespace Compiler
{
}
std::string FileParser::getName() const
{
return mName;
}
Interpreter::Program FileParser::getProgram() const
{
return mScriptParser.getProgram();
@ -39,7 +36,7 @@ namespace Compiler
if (mState == EndNameState)
{
// optional repeated name after end statement
if (mName != name)
if (!Misc::StringUtils::ciEqual(mName, name))
reportWarning("Names for script " + mName + " do not match", loc);
mState = EndCompleteState;
@ -79,7 +76,7 @@ namespace Compiler
if (mState == EndNameState)
{
// optional repeated name after end statement
if (mName != loc.mLiteral)
if (!Misc::StringUtils::ciEqual(mName, loc.mLiteral))
reportWarning("Names for script " + mName + " do not match", loc);
mState = EndCompleteState;

@ -28,9 +28,6 @@ namespace Compiler
public:
FileParser(ErrorHandler& errorHandler, Context& context);
std::string getName() const;
///< Return script name.
Interpreter::Program getProgram() const;
const Locals& getLocals() const;

@ -2,6 +2,11 @@
#define COMPONENTS_LUA_UTIL_H
#include <cstdint>
#include <string>
#include <sol/sol.hpp>
#include <components/esm/refid.hpp>
namespace LuaUtil
{
@ -15,6 +20,13 @@ namespace LuaUtil
{
return i + 1;
}
inline sol::optional<std::string> serializeRefId(ESM::RefId id)
{
if (id.empty())
return sol::nullopt;
return id.serializeText();
}
}
#endif

@ -241,7 +241,7 @@ namespace LuaUtil
return std::make_tuple(angles.x(), angles.z());
};
transMType["getAnglesZYX"] = [](const TransformM& m) {
osg::Vec3f angles = Misc::toEulerAnglesXZ(m.mM);
osg::Vec3f angles = Misc::toEulerAnglesZYX(m.mM);
return std::make_tuple(angles.z(), angles.y(), angles.x());
};
@ -277,7 +277,7 @@ namespace LuaUtil
return std::make_tuple(angles.x(), angles.z());
};
transQType["getAnglesZYX"] = [](const TransformQ& q) {
osg::Vec3f angles = Misc::toEulerAnglesXZ(q.mQ);
osg::Vec3f angles = Misc::toEulerAnglesZYX(q.mQ);
return std::make_tuple(angles.z(), angles.y(), angles.x());
};

@ -36,7 +36,12 @@ namespace Misc
return QIcon();
QFile iconFile(fileName);
iconFile.open(QIODevice::ReadOnly);
if (!iconFile.open(QIODevice::ReadOnly))
{
qDebug() << "Failed to open icon file:" << fileName;
return QIcon();
}
auto content = iconFile.readAll();
if (!content.startsWith("<?xml"))
return QIcon(fileName);

@ -85,7 +85,8 @@ namespace NifBullet
{
if (Misc::StringUtils::ciEqual(node.mName, "Bounding Box"))
{
if (node.mBounds.mType == Nif::BoundingVolume::Type::BOX_BV)
if (node.mBounds.mType == Nif::BoundingVolume::Type::BOX_BV
&& std::ranges::all_of(node.mBounds.mBox.mExtents._v, [](float extent) { return extent > 0.f; }))
{
mShape->mCollisionBox.mExtents = node.mBounds.mBox.mExtents;
mShape->mCollisionBox.mCenter = node.mBounds.mBox.mCenter;

@ -43,7 +43,12 @@ namespace Platform
setStyle("windows");
QFile file(getStyleSheetPath());
file.open(QIODevice::ReadOnly);
if (!file.open(QIODevice::ReadOnly))
{
qDebug() << "Failed to open style sheet file:" << getStyleSheetPath();
return;
}
setStyleSheet(file.readAll());
}
}
@ -60,7 +65,12 @@ namespace Platform
setStyle("windows");
QFile file(getStyleSheetPath());
file.open(QIODevice::ReadOnly);
if (!file.open(QIODevice::ReadOnly))
{
qDebug() << "Failed to open style sheet file:" << getStyleSheetPath();
return;
}
setStyleSheet(file.readAll());
}
else

@ -37,7 +37,8 @@ namespace SceneUtil
osg::StateSet* stateset = node.getOrCreateStateSet();
stateset->setRenderBinDetails(14, "Distortion", osg::StateSet::OVERRIDE_RENDERBIN_DETAILS);
stateset->setNestRenderBins(false);
stateset->setRenderBinDetails(14, "Distortion", osg::StateSet::OVERRIDE_PROTECTED_RENDERBIN_DETAILS);
stateset->addUniform(new osg::Uniform("distortionStrength", distortionStrength));
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);

@ -20,7 +20,6 @@ namespace Settings
SettingValue<std::string> mCharacter{ mIndex, "Saves", "character" };
SettingValue<bool> mAutosave{ mIndex, "Saves", "autosave" };
SettingValue<bool> mTimeplayed{ mIndex, "Saves", "timeplayed" };
SettingValue<int> mMaxQuicksaves{ mIndex, "Saves", "max quicksaves", makeMaxSanitizerInt(1) };
};
}

@ -1,5 +1,5 @@
parse_cmake
sphinx==1.8.5
docutils==0.17.1
sphinx==7.1.2
docutils==0.18.1
jinja2==3.1.4
sphinx_rtd_theme
sphinx_rtd_theme==1.3.0

@ -55,7 +55,7 @@ master_doc = 'index'
# General information about the project.
project = u'OpenMW'
copyright = u'2023, OpenMW Team'
copyright = u'2024, OpenMW Team'
# The version info for the project you're documenting, acts as replacement for

@ -21,19 +21,6 @@ This setting determines whether the game will be automatically saved when the ch
This setting can be toggled in game with the Auto-Save when Rest button in the Prefs panel of the Options menu.
timeplayed
----------
:Type: boolean
:Range: True/False
:Default: False
This setting determines whether the amount of the time the player has spent playing will be displayed
for each saved game in the Load menu. Currently, the counter includes time spent in menus, including the pause menu,
but does not include time spent with the game window minimized.
This setting can only be configured by editing the settings configuration file.
max quicksaves
--------------

@ -1,5 +1,6 @@
local self = require('openmw.self')
local interfaces = require('openmw.interfaces')
local types = require('openmw.types')
local util = require('openmw.util')
local function startPackage(args)
@ -10,6 +11,7 @@ local function startPackage(args)
self:_startAiCombat(args.target, cancelOther)
elseif args.type == 'Pursue' then
if not args.target then error("target required") end
if not types.Player.objectIsInstance(args.target) then error("target must be a player") end
self:_startAiPursue(args.target, cancelOther)
elseif args.type == 'Follow' then
if not args.target then error("target required") end

@ -35,7 +35,8 @@ local function processZoom3rdPerson()
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.ViewMode) or
not Player.getControlSwitch(self, Player.CONTROL_SWITCH.Controls) or
input.getBooleanActionValue('TogglePOV') or
not I.Camera.isModeControlEnabled()
not I.Camera.isModeControlEnabled() or
not I.Camera.isZoomEnabled()
then
return
end

@ -448,7 +448,6 @@ local function resetPlayerGroups()
if not menuGroups[groupKey] then
if groupElements[pageKey][groupKey] then
groupElements[pageKey][groupKey]:destroy()
print(string.format('destroyed group element %s %s', pageKey, groupKey))
groupElements[pageKey][groupKey] = nil
end
page[groupKey] = nil

@ -920,10 +920,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Saves</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>JPG</source>
<translation type="unfinished"></translation>
@ -1415,10 +1411,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Can Zoom on Maps</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Add &quot;Time Played&quot; to Saves</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Notify on Saved Screenshot</source>
<translation type="unfinished"></translation>

@ -1363,14 +1363,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Saves</source>
<translation></translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation></translation>
</message>
<message>
<source>Add &quot;Time Played&quot; to Saves</source>
<translation></translation>
</message>
<message>
<source>Maximum Quicksaves</source>
<translation></translation>

@ -920,10 +920,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Saves</source>
<translation>Sauvegardes</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;body&gt;&lt;p&gt;Cette option affiche le temps de jeu de chaque sauvegarde dans leur menu de sélection.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>JPG</source>
<translation>JPG</translation>
@ -1418,10 +1414,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Can Zoom on Maps</source>
<translation>Permettre le zoom sur la carte</translation>
</message>
<message>
<source>Add &quot;Time Played&quot; to Saves</source>
<translation>Ajoute le temps de jeu aux sauvegardes</translation>
</message>
<message>
<source>Notify on Saved Screenshot</source>
<translation>Notifier l&apos;enregistrement des captures d&apos;écran</translation>

@ -1140,14 +1140,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
<source>Saves</source>
<translation>Сохранения</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Эта настройка определяет, будет ли отображаться время с начала новой игры для выбранного сохранения в меню загрузки.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>Add &quot;Time Played&quot; to Saves</source>
<translation>Выводить &quot;Время в игре&quot; в сохранениях</translation>
</message>
<message>
<source>JPG</source>
<translation>JPG</translation>

@ -933,10 +933,6 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin
<source>Saves</source>
<translation>Sparfiler</translation>
</message>
<message>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This setting determines whether the amount of the time the player has spent playing will be displayed for each saved game in the Load menu.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Denna inställning avgör huruvida mängden tid spelaren har spenderat i spelet kommer visas för varje sparat spel i Ladda spel-menyn.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<source>JPG</source>
<translation>JPG</translation>
@ -1434,10 +1430,6 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin
<source>Can Zoom on Maps</source>
<translation>Kan zooma på kartor</translation>
</message>
<message>
<source>Add &quot;Time Played&quot; to Saves</source>
<translation>Lägg till spelad tid i sparfiler</translation>
</message>
<message>
<source>Notify on Saved Screenshot</source>
<translation>Ge notis vid sparad skärmdump</translation>

@ -349,6 +349,7 @@
---
-- @type ActiveSpellEffect
-- @field #number index Index of this effect within the original list of @{#MagicEffectWithParams} of the spell/enchantment/potion this effect came from.
-- @field #string affectedSkill Optional skill ID
-- @field #string affectedAttribute Optional attribute ID
-- @field #string id Magic effect id
@ -729,7 +730,6 @@
-- @field #number magnitude current magnitude of the effect. Will be set to 0 when effect is removed or expires.
-- @field #number magnitudeBase
-- @field #number magnitudeModifier
-- @field #number index Index of this effect within the original list of @{#MagicEffectWithParams} of the spell/enchantment/potion this effect came from.
--- @{#Sound}: Sounds and Speech
-- @field [parent=#core] #Sound sound

@ -37,7 +37,15 @@
-- @field [parent=#ActorControls] #boolean run true - run, false - walk
-- @field [parent=#ActorControls] #boolean sneak If true - sneak
-- @field [parent=#ActorControls] #boolean jump If true - initiate a jump
-- @field [parent=#ActorControls] #number use if 1 - activates the readied weapon/spell. For weapons, keeping at 1 will charge the attack until set to 0.
-- @field [parent=#ActorControls] #ATTACK_TYPE use Activates the readied weapon/spell according to a provided value. For weapons, keeping this value modified will charge the attack until set to @{#ATTACK_TYPE.NoAttack}. If an @{#ATTACK_TYPE} not appropriate for a currently equipped weapon provided - an appropriate @{#ATTACK_TYPE} will be used instead.
---
-- @type ATTACK_TYPE
-- @field #number NoAttack
-- @field #number Any
-- @field #number Chop
-- @field #number Swing
-- @field #number Thrust
---
-- Enables or disables standard AI (enabled by default).

@ -750,7 +750,7 @@
-- @return #boolean
---
-- Get this item's current enchantment charge.
-- (DEPRECATED, use itemData(item).enchantmentCharge) Get this item's current enchantment charge.
-- @function [parent=#Item] getEnchantmentCharge
-- @param openmw.core#GameObject item
-- @return #number The charge remaining. `nil` if the enchantment has never been used, implying the charge is full. Unenchanted items will always return a value of `nil`.
@ -763,7 +763,7 @@
-- @return #boolean
---
-- Set this item's enchantment charge.
-- (DEPRECATED, use itemData(item).enchantmentCharge) Set this item's enchantment charge.
-- @function [parent=#Item] setEnchantmentCharge
-- @param openmw.core#GameObject item
-- @param #number charge Can be `nil` to reset the unused state / full
@ -777,14 +777,16 @@
-- @return #boolean
---
-- Set of properties that differentiates one item from another of the same record type.
-- Set of properties that differentiates one item from another of the same record type; can be used by any script, but only global and self scripts can change values.
-- @function [parent=#Item] itemData
-- @param openmw.core#GameObject item
-- @return #ItemData
---
-- @type ItemData
-- @field #number condition The item's current condition. Time remaining for lights. Uses left for lockpicks and probes. Current health for weapons and armor.
-- @field #number condition The item's current condition. Time remaining for lights. Uses left for repairs, lockpicks and probes. Current health for weapons and armor.
-- @field #number enchantmentCharge The item's current enchantment charge. Unenchanted items will always return a value of `nil`. Setting this to `nil` will reset the charge of the item.
-- @field #string soul The recordId of the item's current soul. Items without soul will always return a value of `nil`. Setting this to `nil` will remove the soul from the item.
--------------------------------------------------------------------------------
-- @{#Creature} functions
@ -835,7 +837,7 @@
-- @field #string name
-- @field #string baseCreature Record id of a base creature, which was modified to create this one
-- @field #string model VFS path to the creature's model
-- @field #string mwscript
-- @field #string mwscript MWScript on this creature (can be nil)
-- @field #number soulValue The soul value of the creature record
-- @field #number type The @{#Creature.TYPE} of the creature
-- @field #number baseGold The base barter gold of the creature
@ -1115,7 +1117,7 @@
-- @field #string race
-- @field #string class Name of the NPC's class (e. g. Acrobat)
-- @field #string model Path to the model associated with this NPC, used for animations.
-- @field #string mwscript MWScript that is attached to this NPC
-- @field #string mwscript MWScript on this NPC (can be nil)
-- @field #string hair Path to the hair body part model
-- @field #string head Path to the head body part model
-- @field #number baseGold The base barter gold of the NPC
@ -1325,9 +1327,9 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this armor (can be empty)
-- @field #string mwscript MWScript on this armor (can be nil)
-- @field #string icon VFS path to the icon
-- @field #string enchant The enchantment ID of this armor (can be empty)
-- @field #string enchant The enchantment ID of this armor (can be nil)
-- @field #number weight
-- @field #number value
-- @field #number type See @{#Armor.TYPE}
@ -1414,9 +1416,9 @@
-- @field #string id The record ID of the book
-- @field #string name Name of the book
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this book (can be empty)
-- @field #string mwscript MWScript on this book (can be nil)
-- @field #string icon VFS path to the icon
-- @field #string enchant The enchantment ID of this book (can be empty)
-- @field #string enchant The enchantment ID of this book (can be nil)
-- @field #string text The text content of the book
-- @field #number weight
-- @field #number value
@ -1492,9 +1494,9 @@
-- @field #string id Record id
-- @field #string name Name of the clothing
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this clothing (can be empty)
-- @field #string mwscript MWScript on this clothing (can be nil)
-- @field #string icon VFS path to the icon
-- @field #string enchant The enchantment ID of this clothing (can be empty)
-- @field #string enchant The enchantment ID of this clothing (can be nil)
-- @field #number weight
-- @field #number value
-- @field #number type See @{#Clothing.TYPE}
@ -1535,7 +1537,7 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this potion (can be empty)
-- @field #string mwscript MWScript on this potion (can be nil)
-- @field #string icon VFS path to the icon
-- @field #number weight
-- @field #number value
@ -1641,7 +1643,7 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this light (can be empty)
-- @field #string mwscript MWScript on this light (can be nil)
-- @field #string icon VFS path to the icon
-- @field #string sound VFS path to the sound
-- @field #number weight
@ -1689,7 +1691,7 @@
-- @return #MiscellaneousRecord
---
-- Returns the read-only soul of a miscellaneous item
-- (DEPRECATED, use itemData(item).soul) Returns the read-only soul of a miscellaneous item
-- @function [parent=#Miscellaneous] getSoul
-- @param openmw.core#GameObject object
-- @return #string
@ -1702,7 +1704,7 @@
-- @return #MiscellaneousRecord A strongly typed Miscellaneous record.
---
-- Sets the soul of a miscellaneous item, intended for soul gem objects; Must be used in a global script.
-- (DEPRECATED, use itemData(item).soul) Sets the soul of a miscellaneous item, intended for soul gem objects; Must be used in a global script.
-- @function [parent=#Miscellaneous] setSoul
-- @param openmw.core#GameObject object
-- @param #string soulId Record ID for the soul of the creature to use
@ -1712,7 +1714,7 @@
-- @field #string id The record ID of the miscellaneous item
-- @field #string name The name of the miscellaneous item
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this miscellaneous item (can be empty)
-- @field #string mwscript MWScript on this miscellaneous item (can be nil)
-- @field #string icon VFS path to the icon
-- @field #number weight
-- @field #number value
@ -1757,7 +1759,7 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this potion (can be empty)
-- @field #string mwscript MWScript on this potion (can be nil)
-- @field #string icon VFS path to the icon
-- @field #number weight
-- @field #number value
@ -1817,9 +1819,9 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this weapon (can be empty)
-- @field #string mwscript MWScript on this weapon (can be nil)
-- @field #string icon VFS path to the icon
-- @field #string enchant
-- @field #string enchant The enchantment ID of this weapon (can be nil)
-- @field #boolean isMagical
-- @field #boolean isSilver
-- @field #number weight
@ -1886,7 +1888,7 @@
-- @field #string id The record ID of the apparatus
-- @field #string name The name of the apparatus
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this apparatus (can be empty)
-- @field #string mwscript MWScript on this apparatus (can be nil)
-- @field #string icon VFS path to the icon
-- @field #number type The type of apparatus. See @{#Apparatus.TYPE}
-- @field #number weight
@ -1925,7 +1927,7 @@
-- @field #string id The record ID of the lockpick
-- @field #string name The name of the lockpick
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this lockpick (can be empty)
-- @field #string mwscript MWScript on this lockpick (can be nil)
-- @field #string icon VFS path to the icon
-- @field #number maxCondition The maximum number of uses of this lockpick
-- @field #number weight
@ -1964,7 +1966,7 @@
-- @field #string id The record ID of the probe
-- @field #string name The name of the probe
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this probe (can be empty)
-- @field #string mwscript MWScript on this probe (can be nil)
-- @field #string icon VFS path to the icon
-- @field #number maxCondition The maximum number of uses of this probe
-- @field #number weight
@ -2003,7 +2005,7 @@
-- @field #string id The record ID of the repair tool
-- @field #string name The name of the repair tool
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this repair tool (can be empty)
-- @field #string mwscript MWScript on this repair tool (can be nil)
-- @field #string icon VFS path to the icon
-- @field #number maxCondition The maximum number of uses of this repair tool
-- @field #number weight
@ -2040,7 +2042,7 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this activator (can be empty)
-- @field #string mwscript MWScript on this activator (can be nil)
---
-- Creates a @{#ActivatorRecord} without adding it to the world database.
@ -2107,7 +2109,7 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this container (can be empty)
-- @field #string mwscript MWScript on this container (can be nil)
-- @field #number weight capacity of this container
-- @field #boolean isOrganic Whether items can be placed in the container
-- @field #boolean isRespawning Whether the container respawns its contents
@ -2169,10 +2171,9 @@
-- @field #string id Record id
-- @field #string name Human-readable name
-- @field #string model VFS path to the model
-- @field #string mwscript MWScript on this door (can be empty)
-- @field #string openSound VFS path to the sound of opening
-- @field #string closeSound VFS path to the sound of closing
-- @field #string mwscript MWScript on this door (can be nil)
-- @field #string openSound The sound id for door opening
-- @field #string closeSound The sound id for door closing
--- Functions for @{#Static} objects

@ -8,8 +8,8 @@
<string>English</string>
<key>CFBundleExecutable</key>
<string>openmw-launcher</string>
<key>CFBundleIdentifier</key>
<string>org.openmw.openmw</string>
<key>CFBundleIdentifier</key>
<string>org.openmw.openmw</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
@ -20,14 +20,16 @@
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>${OPENMW_VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${OPENMW_VERSION}</string>
<key>CFBundleVersion</key>
<string>${OPENMW_VERSION}</string>
<key>CSResourcesFileMapped</key>
<true/>
<key>LSRequiresCarbon</key>
<true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.role-playing-games</string>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>NSHighResolutionCapable</key>

@ -566,9 +566,6 @@ character =
# Automatically save the game whenever the player rests.
autosave = true
# Display the time played on each save file in the load menu.
timeplayed = false
# The maximum number of quick (or auto) save slots to have.
# If all slots are used, the oldest save is reused
max quicksaves = 1

@ -13,18 +13,88 @@ types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Magic, false)
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.VanityMode, false)
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.ViewMode, false)
testing.registerLocalTest('playerRotation',
function()
local endTime = core.getSimulationTime() + 1
while core.getSimulationTime() < endTime do
self.controls.jump = false
self.controls.run = true
self.controls.movement = 0
self.controls.sideMovement = 0
self.controls.yawChange = util.normalizeAngle(math.rad(90) - self.rotation:getYaw()) * 0.5
coroutine.yield()
local function rotate(object, targetPitch, targetYaw)
local endTime = core.getSimulationTime() + 1
while core.getSimulationTime() < endTime do
object.controls.jump = false
object.controls.run = true
object.controls.movement = 0
object.controls.sideMovement = 0
if targetPitch ~= nil then
object.controls.pitchChange = util.normalizeAngle(targetPitch - object.rotation:getPitch()) * 0.5
end
if targetYaw ~= nil then
object.controls.yawChange = util.normalizeAngle(targetYaw - object.rotation:getYaw()) * 0.5
end
testing.expectEqualWithDelta(self.rotation:getYaw(), math.rad(90), 0.05, 'Incorrect rotation')
coroutine.yield()
end
end
local function rotateByYaw(object, target)
rotate(object, nil, target)
end
local function rotateByPitch(object, target)
rotate(object, target, nil)
end
testing.registerLocalTest('playerYawRotation',
function()
local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ()
local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX()
local targetYaw = math.rad(90)
rotateByYaw(self, targetYaw)
testing.expectEqualWithDelta(self.rotation:getYaw(), targetYaw, 0.05, 'Incorrect yaw rotation')
local alpha1, gamma1 = self.rotation:getAnglesXZ()
testing.expectEqualWithDelta(alpha1, initialAlphaXZ, 0.05, 'Alpha rotation in XZ convention should not change')
testing.expectEqualWithDelta(gamma1, targetYaw, 0.05, 'Incorrect gamma rotation in XZ convention')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX()
testing.expectEqualWithDelta(alpha2, targetYaw, 0.05, 'Incorrect alpha rotation in ZYX convention')
testing.expectEqualWithDelta(beta2, initialBetaZYX, 0.05, 'Beta rotation in ZYX convention should not change')
testing.expectEqualWithDelta(gamma2, initialGammaZYX, 0.05, 'Gamma rotation in ZYX convention should not change')
end)
testing.registerLocalTest('playerPitchRotation',
function()
local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ()
local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX()
local targetPitch = math.rad(90)
rotateByPitch(self, targetPitch)
testing.expectEqualWithDelta(self.rotation:getPitch(), targetPitch, 0.05, 'Incorrect pitch rotation')
local alpha1, gamma1 = self.rotation:getAnglesXZ()
testing.expectEqualWithDelta(alpha1, targetPitch, 0.05, 'Incorrect alpha rotation in XZ convention')
testing.expectEqualWithDelta(gamma1, initialGammaXZ, 0.05, 'Gamma rotation in XZ convention should not change')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX()
testing.expectEqualWithDelta(alpha2, initialAlphaZYX, 0.05, 'Alpha rotation in ZYX convention should not change')
testing.expectEqualWithDelta(beta2, initialBetaZYX, 0.05, 'Beta rotation in ZYX convention should not change')
testing.expectEqualWithDelta(gamma2, targetPitch, 0.05, 'Incorrect gamma rotation in ZYX convention')
end)
testing.registerLocalTest('playerPitchAndYawRotation',
function()
local targetPitch = math.rad(-30)
local targetYaw = math.rad(-60)
rotate(self, targetPitch, targetYaw)
testing.expectEqualWithDelta(self.rotation:getPitch(), targetPitch, 0.05, 'Incorrect pitch rotation')
testing.expectEqualWithDelta(self.rotation:getYaw(), targetYaw, 0.05, 'Incorrect yaw rotation')
local alpha1, gamma1 = self.rotation:getAnglesXZ()
testing.expectEqualWithDelta(alpha1, targetPitch, 0.05, 'Incorrect alpha rotation in XZ convention')
testing.expectEqualWithDelta(gamma1, targetYaw, 0.05, 'Incorrect gamma rotation in XZ convention')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX()
testing.expectEqualWithDelta(alpha2, math.rad(-56), 0.05, 'Incorrect alpha rotation in ZYX convention')
testing.expectEqualWithDelta(beta2, math.rad(-25), 0.05, 'Incorrect beta rotation in ZYX convention')
testing.expectEqualWithDelta(gamma2, math.rad(-16), 0.05, 'Incorrect gamma rotation in ZYX convention')
end)
testing.registerLocalTest('playerForwardRunning',

@ -44,7 +44,17 @@ local function testTeleport()
testing.expectEqualWithDelta(player.position.x, 100, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.position.y, 50, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.position.z, 500, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(90), 0.05, 'incorrect rotation after teleporting')
testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(90), 0.05, 'incorrect yaw rotation after teleporting')
testing.expectEqualWithDelta(player.rotation:getPitch(), math.rad(0), 0.05, 'incorrect pitch rotation after teleporting')
local rotationX1, rotationZ1 = player.rotation:getAnglesXZ()
testing.expectEqualWithDelta(rotationX1, math.rad(0), 0.05, 'incorrect x rotation from getAnglesXZ after teleporting')
testing.expectEqualWithDelta(rotationZ1, math.rad(90), 0.05, 'incorrect z rotation from getAnglesXZ after teleporting')
local rotationZ2, rotationY2, rotationX2 = player.rotation:getAnglesZYX()
testing.expectEqualWithDelta(rotationZ2, math.rad(90), 0.05, 'incorrect z rotation from getAnglesZYX after teleporting')
testing.expectEqualWithDelta(rotationY2, math.rad(0), 0.05, 'incorrect y rotation from getAnglesZYX after teleporting')
testing.expectEqualWithDelta(rotationX2, math.rad(0), 0.05, 'incorrect x rotation from getAnglesZYX after teleporting')
player:teleport('', player.position, {rotation=util.transform.rotateZ(math.rad(-90)), onGround=true})
coroutine.yield()
@ -193,9 +203,17 @@ end
tests = {
{'timers', testTimers},
{'playerRotation', function()
{'rotating player with controls.yawChange should change rotation', function()
initPlayer()
testing.runLocalTest(player, 'playerYawRotation')
end},
{'rotating player with controls.pitchChange should change rotation', function()
initPlayer()
testing.runLocalTest(player, 'playerPitchRotation')
end},
{'rotating player with controls.pitchChange and controls.yawChange should change rotation', function()
initPlayer()
testing.runLocalTest(player, 'playerRotation')
testing.runLocalTest(player, 'playerPitchAndYawRotation')
end},
{'playerForwardRunning', function()
initPlayer()

Loading…
Cancel
Save