diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 140eae022d..712cf60115 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -429,7 +429,11 @@ Ubuntu_Clang: - mkdir -pv "${CCACHE_DIR}" - ccache -z -M "${CCACHE_SIZE}" - CI/before_script.linux.sh - - cp extern/.clang-tidy build/.clang-tidy + - mkdir -p build/extern build/apps/launcher build/apps/opencs build/apps/wizard + - cp extern/.clang-tidy build/extern/ + - cp extern/.clang-tidy build/apps/launcher/ + - cp extern/.clang-tidy build/apps/opencs/ + - cp extern/.clang-tidy build/apps/wizard/ - cd build - find . -name *.o -exec touch {} \; - cmake --build . -- -j $(nproc) ${BUILD_TARGETS} diff --git a/CMakeLists.txt b/CMakeLists.txt index 3565d9af56..b1ab2d643e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 51) set(OPENMW_VERSION_RELEASE 0) -set(OPENMW_LUA_API_REVISION 99) +set(OPENMW_LUA_API_REVISION 101) set(OPENMW_POSTPROCESSING_API_REVISION 3) set(OPENMW_VERSION_COMMITHASH "") diff --git a/apps/components_tests/lua/testutilpackage.cpp b/apps/components_tests/lua/testutilpackage.cpp index 47041985f6..11874a2cea 100644 --- a/apps/components_tests/lua/testutilpackage.cpp +++ b/apps/components_tests/lua/testutilpackage.cpp @@ -158,6 +158,13 @@ namespace EXPECT_EQ(get(lua, "darkRed:asHex()"), "a01112"); EXPECT_TRUE(get(lua, "green:asRgba() == util.vector4(0, 1, 0, 1)")); EXPECT_TRUE(get(lua, "red:asRgb() == util.vector3(1, 0, 0)")); + lua.safe_script("green = util.color.commaString('0,255,0')"); + EXPECT_EQ(get(lua, "tostring(green)"), "(0, 1, 0, 1)"); + lua.safe_script("red = util.color.commaString('255, 0, 0, 255')"); + EXPECT_EQ(get(lua, "tostring(red)"), "(1, 0, 0, 1)"); + lua.safe_script("blue = util.color.commaString('0, 0, 1000, 255')"); + EXPECT_EQ(get(lua, "tostring(blue)"), "(0, 0, 1, 1)"); + EXPECT_ERROR(lua.safe_script("white = util.color.commaString('aaa')"), "Invalid comma-separated color"); } TEST_F(LuaUtilPackageTest, Transform) diff --git a/apps/openmw/mwgui/itemview.cpp b/apps/openmw/mwgui/itemview.cpp index dc3772a4d2..ceaedf3b75 100644 --- a/apps/openmw/mwgui/itemview.cpp +++ b/apps/openmw/mwgui/itemview.cpp @@ -82,9 +82,10 @@ namespace MWGui if (Settings::gui().mControllerMenus) { - mControllerFocus = -1; if (mItemCount > 0) mControllerFocus = std::clamp(mControllerFocus, 0, mItemCount - 1); + else + mControllerFocus = -1; updateControllerFocus(-1, mControllerFocus); } diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 28416a10ba..520f09cad0 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -222,21 +222,23 @@ namespace MWLua // Run event handlers for events that were sent before `finalizeEventBatch`. mLuaEvents.callEventHandlers(); - // Run queued callbacks - for (CallbackWithData& c : mQueuedCallbacks) - c.mCallback.tryCall(c.mArg); - mQueuedCallbacks.clear(); + mLua.protectedCall([&](LuaUtil::LuaView& lua) { + // Run queued callbacks + for (CallbackWithData& c : mQueuedCallbacks) + c.mCallback.tryCall(c.mArg); + mQueuedCallbacks.clear(); - // Run engine handlers - mEngineEvents.callEngineHandlers(); - bool isPaused = timeManager.isPaused(); + // Run engine handlers + mEngineEvents.callEngineHandlers(); + bool isPaused = timeManager.isPaused(); - float frameDuration = MWBase::Environment::get().getFrameDuration(); - for (LocalScripts* scripts : mActiveLocalScripts) - scripts->update(isPaused ? 0 : frameDuration); - mGlobalScripts.update(isPaused ? 0 : frameDuration); + float frameDuration = MWBase::Environment::get().getFrameDuration(); + for (LocalScripts* scripts : mActiveLocalScripts) + scripts->update(isPaused ? 0 : frameDuration); + mGlobalScripts.update(isPaused ? 0 : frameDuration); - mLua.protectedCall([&](LuaUtil::LuaView& lua) { mScriptTracker.unloadInactiveScripts(lua); }); + mScriptTracker.unloadInactiveScripts(lua); + }); } void LuaManager::objectTeleported(const MWWorld::Ptr& ptr) diff --git a/apps/openmw/mwlua/nearbybindings.cpp b/apps/openmw/mwlua/nearbybindings.cpp index c1ef54fe7f..48b1663e84 100644 --- a/apps/openmw/mwlua/nearbybindings.cpp +++ b/apps/openmw/mwlua/nearbybindings.cpp @@ -26,18 +26,27 @@ namespace { std::vector ignore; - if (const auto& ignoreObj = options.get>("ignore")) + if (const auto& ignoreObj = options.get>("ignore")) { - ignore.push_back(ignoreObj->ptr()); - } - else if (const auto& ignoreTable = options.get>("ignore")) - { - ignoreTable->for_each([&](const auto& _, const sol::object& value) { - if (value.is()) + if (ignoreObj->is()) + ignore.push_back(ignoreObj->as().ptr()); + else if (ignoreObj->is()) + { + for (const MWLua::ObjectId& id : *ignoreObj->as().mIds) { - ignore.push_back(value.as().ptr()); + ignore.push_back(MWLua::LObject(id).ptr()); } - }); + } + else + { + // ignoreObj->as throws if the type doesn't match, but an unchecked value.as crashes... + ignoreObj->as().for_each([&](sol::object _, sol::object value) { + if (value.is()) + ignore.push_back(value.as().ptr()); + else + throw std::runtime_error("Table value is not a GameObject"); + }); + } } return ignore; diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index c023728dbd..e0bed6f8a1 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -77,6 +77,7 @@ namespace MWLua luaManager->addAction([state] { MWBase::Environment::get().getWindowManager()->setHudVisibility(state); }); }; api["_isHudVisible"] = []() -> bool { return MWBase::Environment::get().getWindowManager()->isHudVisible(); }; + api["_getDefaultFontSize"] = []() -> int { return Settings::gui().mFontSize; }; api["showMessage"] = [luaManager = context.mLuaManager](std::string_view message, const sol::optional& options) { MWGui::ShowInDialogueMode mode = MWGui::ShowInDialogueMode_IfPossible; diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 1e19390c67..e82ff0c8d2 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -1,6 +1,8 @@ #include "combat.hpp" +#include + #include #include @@ -504,15 +506,19 @@ namespace MWMechanics } MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); + auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (isWerewolf) { - auto& prng = MWBase::Environment::get().getWorld()->getPrng(); const ESM::Sound* sound = store.get().searchRandom("WolfHit", prng); if (sound) sndMgr->playSound3D(victim, sound->mId, 1.0f, 1.0f); } else if (!healthdmg) - sndMgr->playSound3D(victim, ESM::RefId::stringRefId("Hand To Hand Hit"), 1.0f, 1.0f); + { + static const std::array sounds + = { ESM::RefId::stringRefId("Hand To Hand Hit"), ESM::RefId::stringRefId("Hand To Hand Hit 2") }; + sndMgr->playSound3D(victim, sounds[Misc::Rng::rollDice(sounds.size(), prng)], 1.0f, 1.0f); + } } void applyFatigueLoss(const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float attackStrength) diff --git a/apps/openmw/mwmechanics/disease.hpp b/apps/openmw/mwmechanics/disease.hpp index 262f813916..4e36e47ca0 100644 --- a/apps/openmw/mwmechanics/disease.hpp +++ b/apps/openmw/mwmechanics/disease.hpp @@ -21,11 +21,11 @@ namespace MWMechanics { /// Call when \a actor has got in contact with \a carrier (e.g. hit by him, or loots him) - /// @param actor The actor that will potentially catch diseases. Currently only the player can catch diseases. + /// @param actor The actor that will potentially catch diseases. Actors cannot catch diseases from the player. /// @param carrier The disease carrier. inline void diseaseContact(const MWWorld::Ptr& actor, const MWWorld::Ptr& carrier) { - if (!carrier.getClass().isActor() || actor != getPlayer()) + if (!carrier.getClass().isActor() || carrier == getPlayer()) return; float fDiseaseXferChance = MWBase::Environment::get() @@ -71,13 +71,16 @@ namespace MWMechanics creatureStats.getActiveSpells().addSpell(spell, actor, false); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); - std::string msg = MWBase::Environment::get() - .getESMStore() - ->get() - .find("sMagicContractDisease") - ->mValue.getString(); - msg = Misc::StringUtils::format(msg, spell->mName); - MWBase::Environment::get().getWindowManager()->messageBox(msg); + if (actor == getPlayer()) + { + std::string msg = MWBase::Environment::get() + .getESMStore() + ->get() + .find("sMagicContractDisease") + ->mValue.getString(); + msg = Misc::StringUtils::format(msg, spell->mName); + MWBase::Environment::get().getWindowManager()->messageBox(msg); + } } } } diff --git a/apps/openmw_tests/mwscript/testscripts.cpp b/apps/openmw_tests/mwscript/testscripts.cpp index b9e422daed..561a7bc3fa 100644 --- a/apps/openmw_tests/mwscript/testscripts.cpp +++ b/apps/openmw_tests/mwscript/testscripts.cpp @@ -1,4 +1,6 @@ #include + +#include #include #include "testutils.hpp" @@ -126,6 +128,34 @@ player -> addSpell "fire_bite", 645 PositionCell "Rabenfels, Taverne" 4480.000 3968.000 15820.000 0 +End)mwscript"; + + const std::string sScript5 = R"mwscript(Begin messagebox_format_script + +float fVal + +set fVal to 12.34 + +MessageBox "hello world" +MessageBox "%.0f" fVal +MessageBox "%.f" fVal +MessageBox "a %03.0f b" fVal +MessageBox "%+04.0f" fVal +MessageBox "%+4.0f" fVal +MessageBox "%+ 4.0f" fVal +MessageBox "%0+ 4.0f" fVal +MessageBox "%0+ #4.0f" fVal +MessageBox "%- 5.0f" fVal + +MessageBox "%g" fVal +MessageBox "%.3g" fVal +MessageBox "%.5g" fVal +MessageBox "%#.5g" fVal +MessageBox "%-5g" fVal +MessageBox "%- 5g" fVal + +MessageBox "%.1b" fVal + End)mwscript"; const std::string sIssue587 = R"mwscript(Begin stalresetScript @@ -579,6 +609,47 @@ End)mwscript"; EXPECT_FALSE(!compile(sScript4)); } + TEST_F(MWScriptTest, mwscript_test_messagebox_format) + { + if (const auto script = compile(sScript5)) + { + TestInterpreterContext context; + run(*script, context); + using std::string_view_literals::operator""sv; + constexpr std::array expected{ + "hello world"sv, + "12"sv, + "12"sv, + "a 012 b"sv, + "+012"sv, + " +12"sv, + " +12"sv, + "+012"sv, + "+12."sv, + " 12 "sv, + + "12.34"sv, + "12.3"sv, + "12.34"sv, + "12.340"sv, + "12.34"sv, + " 12.34"sv, + + "b"sv, + }; + const std::vector& output = context.getMessages(); + EXPECT_EQ(expected.size(), output.size()); + for (std::size_t i = 0; i < output.size(); i++) + { + EXPECT_EQ(expected[i], output[i]); + } + } + else + { + FAIL(); + } + } + TEST_F(MWScriptTest, mwscript_test_587) { EXPECT_FALSE(!compile(sIssue587)); diff --git a/apps/openmw_tests/mwscript/testutils.hpp b/apps/openmw_tests/mwscript/testutils.hpp index ba22156f56..8ad571b6ee 100644 --- a/apps/openmw_tests/mwscript/testutils.hpp +++ b/apps/openmw_tests/mwscript/testutils.hpp @@ -145,8 +145,11 @@ namespace { LocalVariables mLocals; std::map mMembers; + std::vector mMessages; public: + const std::vector& getMessages() { return mMessages; } + ESM::RefId getTarget() const override { return ESM::RefId(); } int getLocalShort(int index) const override { return mLocals.getShort(index); } @@ -161,7 +164,10 @@ namespace void setLocalFloat(int index, float value) override { mLocals.setFloat(index, value); } - void messageBox(std::string_view message, const std::vector& buttons) override {} + void messageBox(std::string_view message, const std::vector& buttons) override + { + mMessages.emplace_back(message); + } void report(const std::string& message) override {} diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index 90bdac1610..6ea4f9c66a 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -481,7 +481,7 @@ namespace Compiler } void GetArgumentsFromMessageFormat::visitedPlaceholder( - Placeholder placeholder, char /*padding*/, int /*width*/, int /*precision*/, Notation /*notation*/) + Placeholder placeholder, int /*flags*/, int /*width*/, int /*precision*/, Notation /*notation*/) { switch (placeholder) { diff --git a/components/compiler/lineparser.hpp b/components/compiler/lineparser.hpp index 68b3198ff2..67ad8e7603 100644 --- a/components/compiler/lineparser.hpp +++ b/components/compiler/lineparser.hpp @@ -88,7 +88,7 @@ namespace Compiler protected: void visitedPlaceholder( - Placeholder placeholder, char padding, int width, int precision, Notation notation) override; + Placeholder placeholder, int flags, int width, int precision, Notation notation) override; void visitedCharacter(char c) override {} public: diff --git a/components/interpreter/miscopcodes.hpp b/components/interpreter/miscopcodes.hpp index 0b49b3c248..05791617a6 100644 --- a/components/interpreter/miscopcodes.hpp +++ b/components/interpreter/miscopcodes.hpp @@ -2,8 +2,7 @@ #define INTERPRETER_MISCOPCODES_H_INCLUDED #include -#include -#include +#include #include #include @@ -23,78 +22,84 @@ namespace Interpreter protected: void visitedPlaceholder( - Placeholder placeholder, char padding, int width, int precision, Notation notation) override + Placeholder placeholder, int flags, int width, int precision, Notation notation) override { - std::ostringstream out; - out.fill(padding); - if (width != -1) - out.width(width); - if (precision != -1) - out.precision(precision); + std::string formatString; - switch (placeholder) + if (placeholder == StringPlaceholder) { - case StringPlaceholder: - { - int index = mRuntime[0].mInteger; - mRuntime.pop(); + int index = mRuntime[0].mInteger; + mRuntime.pop(); - out << mRuntime.getStringLiteral(index); - mFormattedMessage += out.str(); - } - break; - case IntegerPlaceholder: + std::string_view value = mRuntime.getStringLiteral(index); + if (precision >= 0) + value = value.substr(0, static_cast(precision)); + if (width < 0) + mFormattedMessage += value; + else { - Type_Integer value = mRuntime[0].mInteger; - mRuntime.pop(); - - out << value; - mFormattedMessage += out.str(); + formatString = "{:"; + if (flags & PrependZero) + formatString += '0'; + if (flags & AlignLeft) + formatString += '<'; + else + formatString += '>'; + formatString += "{}}"; + std::vformat_to( + std::back_inserter(mFormattedMessage), formatString, std::make_format_args(value, width)); } - break; - case FloatPlaceholder: + } + else + { + formatString = "{:"; + if (flags & AlignLeft) + formatString += '<'; + if (flags & PositiveSign) + formatString += '+'; + else if (flags & PositiveSpace) + formatString += ' '; + if (flags & AlternateForm) + formatString += '#'; + if (flags & PrependZero) + formatString += '0'; + if (width >= 0) + formatString += "{}"; + if (placeholder == FloatPlaceholder) + { + if (precision >= 0) + formatString += ".{}"; + formatString += static_cast(notation); + } + else + precision = -1; + formatString += '}'; + const auto appendMessage = [&](auto value) { + if (width >= 0 && precision >= 0) + std::vformat_to(std::back_inserter(mFormattedMessage), formatString, + std::make_format_args(value, width, precision)); + else if (width >= 0) + std::vformat_to( + std::back_inserter(mFormattedMessage), formatString, std::make_format_args(value, width)); + else if (precision >= 0) + std::vformat_to(std::back_inserter(mFormattedMessage), formatString, + std::make_format_args(value, precision)); + else + std::vformat_to( + std::back_inserter(mFormattedMessage), formatString, std::make_format_args(value)); + }; + if (placeholder == FloatPlaceholder) { float value = mRuntime[0].mFloat; mRuntime.pop(); - - if (notation == Notation::Fixed) - { - out << std::fixed << value; - mFormattedMessage += out.str(); - } - else if (notation == Notation::Shortest) - { - out << value; - std::string standard = out.str(); - - out.str(std::string()); - out.clear(); - - out << std::scientific << value; - std::string scientific = out.str(); - - mFormattedMessage += standard.length() < scientific.length() ? standard : scientific; - } - // TODO switch to std::format so the precision argument applies to these two - else if (notation == Notation::HexLower) - { - out << std::hexfloat << value; - mFormattedMessage += out.str(); - } - else if (notation == Notation::HexUpper) - { - out << std::uppercase << std::hexfloat << value; - mFormattedMessage += out.str(); - } - else - { - out << std::scientific << value; - mFormattedMessage += out.str(); - } + appendMessage(value); + } + else + { + Type_Integer value = mRuntime[0].mInteger; + mRuntime.pop(); + appendMessage(value); } - break; - default: - break; } } diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp index 178d19aef2..1a432ec749 100644 --- a/components/lua/luastate.hpp +++ b/components/lua/luastate.hpp @@ -50,7 +50,8 @@ namespace LuaUtil } public: - friend class LuaState; + template + friend int invokeProtectedCall(lua_State*, Function&&); // Returns underlying sol::state. sol::state_view& sol() { return mSol; } @@ -67,6 +68,45 @@ namespace LuaUtil return res; } + // Pushing to the stack from outside a Lua context crashes the engine if no memory can be allocated to grow the + // stack + template + [[nodiscard]] int invokeProtectedCall(lua_State* luaState, Function&& function) + { + if (!lua_checkstack(luaState, 2)) + return LUA_ERRMEM; + lua_pushcfunction(luaState, [](lua_State* state) { + void* f = lua_touserdata(state, 1); + LuaView view(state); + (*static_cast(f))(view); + return 0; + }); + lua_pushlightuserdata(luaState, &function); + return lua_pcall(luaState, 1, 0, 0); + } + + template + void protectedCall(lua_State* luaState, Lambda&& f) + { + int result = invokeProtectedCall(luaState, std::forward(f)); + switch (result) + { + case LUA_OK: + break; + case LUA_ERRMEM: + throw std::runtime_error("Lua error: out of memory"); + case LUA_ERRRUN: + { + sol::optional error = sol::stack::check_get(luaState); + if (error) + throw std::runtime_error(*error); + } + [[fallthrough]]; + default: + throw std::runtime_error("Lua error: " + std::to_string(result)); + } + } + // Holds Lua state. // Provides additional features: // - Load scripts from the virtual filesystem; @@ -87,43 +127,10 @@ namespace LuaUtil LuaState(const LuaState&) = delete; LuaState(LuaState&&) = delete; - // Pushing to the stack from outside a Lua context crashes the engine if no memory can be allocated to grow the - // stack - template - [[nodiscard]] int invokeProtectedCall(Function&& function) const - { - if (!lua_checkstack(mSol.lua_state(), 2)) - return LUA_ERRMEM; - lua_pushcfunction(mSol.lua_state(), [](lua_State* state) { - void* f = lua_touserdata(state, 1); - LuaView view(state); - (*static_cast(f))(view); - return 0; - }); - lua_pushlightuserdata(mSol.lua_state(), &function); - return lua_pcall(mSol.lua_state(), 1, 0, 0); - } - template void protectedCall(Lambda&& f) const { - int result = invokeProtectedCall(std::forward(f)); - switch (result) - { - case LUA_OK: - break; - case LUA_ERRMEM: - throw std::runtime_error("Lua error: out of memory"); - case LUA_ERRRUN: - { - sol::optional error = sol::stack::check_get(mSol.lua_state()); - if (error) - throw std::runtime_error(*error); - } - [[fallthrough]]; - default: - throw std::runtime_error("Lua error: " + std::to_string(result)); - } + LuaUtil::protectedCall(mSol.lua_state(), std::forward(f)); } // Note that constructing a sol::state_view is only safe from a Lua context. Use protectedCall to get one diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 663aca8842..82eaa47ec9 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -407,6 +407,16 @@ namespace LuaUtil } void ScriptsContainer::save(ESM::LuaScripts& data) + { + if (const UnloadedData* unloadedData = std::get_if(&mData)) + { + data.mScripts = unloadedData->mScripts; + return; + } + mLua.protectedCall([&](LuaView& view) { save(view, data); }); + } + + void ScriptsContainer::save(LuaView&, ESM::LuaScripts& data) { if (const UnloadedData* unloadedData = std::get_if(&mData)) { @@ -614,12 +624,12 @@ namespace LuaUtil return data; } - ScriptsContainer::UnloadedData& ScriptsContainer::ensureUnloaded(LuaView&) + ScriptsContainer::UnloadedData& ScriptsContainer::ensureUnloaded(LuaView& view) { if (UnloadedData* data = std::get_if(&mData)) return *data; UnloadedData data; - save(data); + save(view, data); mAPI.erase("openmw.interfaces"); UnloadedData& out = mData.emplace(std::move(data)); for (auto& [_, handlers] : mEngineHandlers) @@ -751,9 +761,11 @@ namespace LuaUtil void ScriptsContainer::processTimers(double simulationTime, double gameTime) { - LoadedData& data = ensureLoaded(); - updateTimerQueue(data.mSimulationTimersQueue, simulationTime); - updateTimerQueue(data.mGameTimersQueue, gameTime); + mLua.protectedCall([&](LuaView& view) { + LoadedData& data = ensureLoaded(); + updateTimerQueue(data.mSimulationTimersQueue, simulationTime); + updateTimerQueue(data.mGameTimersQueue, gameTime); + }); } static constexpr float instructionCountAvgCoef = 1.0f / 30; // averaging over approximately 30 frames diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 275c300ac9..fe036bae8e 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -286,6 +286,7 @@ namespace LuaUtil static void removeHandler(std::vector& list, int scriptId); void insertInterface(int scriptId, const Script& script); void removeInterface(int scriptId, const Script& script); + void save(LuaView&, ESM::LuaScripts&); ScriptIdsWithInitializationData mAutoStartScripts; const UserdataSerializer* mSerializer = nullptr; diff --git a/components/lua/util.hpp b/components/lua/util.hpp index e664dc1f34..d3cb764831 100644 --- a/components/lua/util.hpp +++ b/components/lua/util.hpp @@ -2,7 +2,6 @@ #define COMPONENTS_LUA_UTIL_H #include -#include #include @@ -24,11 +23,11 @@ namespace LuaUtil // ADL-based customization point for sol2 to automatically convert ESM::RefId // Empty RefIds are converted to nil, non-empty ones are serialized to strings -inline int sol_lua_push(sol::types, lua_State* L, const ESM::RefId& id) +inline int sol_lua_push(sol::types, lua_State* state, const ESM::RefId& id) { if (id.empty()) - return sol::stack::push(L, sol::lua_nil); - return sol::stack::push(L, id.serializeText()); + return sol::stack::push(state, sol::lua_nil); + return sol::stack::push(state, id.serializeText()); } -#endif \ No newline at end of file +#endif diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp index 0117934def..c4a8230def 100644 --- a/components/lua/utilpackage.cpp +++ b/components/lua/utilpackage.cpp @@ -8,6 +8,9 @@ #include #include +#include + +#include #include "luastate.hpp" #include "util.hpp" @@ -243,6 +246,28 @@ namespace LuaUtil color["rgba"] = [](float r, float g, float b, float a) { return Misc::Color(r, g, b, a); }; color["rgb"] = [](float r, float g, float b) { return Misc::Color(r, g, b, 1); }; color["hex"] = [](std::string_view hex) { return Misc::Color::fromHex(hex); }; + color["commaString"] = [](std::string_view str) { + int wrongChars = std::count_if( + str.begin(), str.end(), [](unsigned char c) { return !std::isdigit(c) && c != ' ' && c != ','; }); + + if (wrongChars != 0) + { + throw std::runtime_error("Invalid comma-separated color: " + std::string(str)); + } + + std::vector rgba; + Misc::StringUtils::split(str, rgba, ","); + if (rgba.size() != 3 && rgba.size() != 4) + { + throw std::runtime_error("Invalid comma-separated color: " + std::string(str)); + } + + if (rgba.size() == 3) + rgba.push_back("255"); + + return Misc::Color(MyGUI::utility::parseInt(rgba[0]) / 255.f, MyGUI::utility::parseInt(rgba[1]) / 255.f, + MyGUI::utility::parseInt(rgba[2]) / 255.f, MyGUI::utility::parseInt(rgba[3]) / 255.f); + }; util["color"] = LuaUtil::makeReadOnly(color); // Lua bindings for Transform diff --git a/components/lua_ui/textedit.cpp b/components/lua_ui/textedit.cpp index 9bd241884a..43f7c09cce 100644 --- a/components/lua_ui/textedit.cpp +++ b/components/lua_ui/textedit.cpp @@ -46,7 +46,9 @@ namespace LuaUi void LuaTextEdit::textChange(MyGUI::EditBox*) { - triggerEvent("textChanged", sol::make_object(lua(), mEditBox->getCaption().asUTF8())); + protectedCall([this](LuaUtil::LuaView& view) { + triggerEvent("textChanged", sol::make_object(view.sol(), mEditBox->getCaption().asUTF8())); + }); } void LuaTextEdit::updateCoord() diff --git a/components/lua_ui/widget.cpp b/components/lua_ui/widget.cpp index 0429d5aecb..5b7d52bc51 100644 --- a/components/lua_ui/widget.cpp +++ b/components/lua_ui/widget.cpp @@ -169,28 +169,23 @@ namespace LuaUi return result; } - sol::table WidgetExtension::makeTable() const - { - return sol::table(lua(), sol::create); - } - - sol::object WidgetExtension::keyEvent(MyGUI::KeyCode code) const + sol::object WidgetExtension::keyEvent(LuaUtil::LuaView& view, MyGUI::KeyCode code) const { auto keySym = SDL_Keysym(); keySym.sym = SDLUtil::myGuiKeyToSdl(code); keySym.scancode = SDL_GetScancodeFromKey(keySym.sym); keySym.mod = static_cast(SDL_GetModState()); - return sol::make_object(lua(), keySym); + return sol::make_object(view.sol(), keySym); } sol::object WidgetExtension::mouseEvent( - int left, int top, MyGUI::MouseButton button = MyGUI::MouseButton::None) const + LuaUtil::LuaView& view, int left, int top, MyGUI::MouseButton button = MyGUI::MouseButton::None) const { osg::Vec2f position(static_cast(left), static_cast(top)); MyGUI::IntPoint absolutePosition = mWidget->getAbsolutePosition(); osg::Vec2f offset = position - osg::Vec2f(static_cast(absolutePosition.left), static_cast(absolutePosition.top)); - sol::table table = makeTable(); + sol::table table = view.newTable(); int sdlButton = SDLUtil::myGuiMouseButtonToSdl(button); table["position"] = position; table["offset"] = offset; @@ -373,31 +368,39 @@ namespace LuaUi void WidgetExtension::keyPress(MyGUI::Widget*, MyGUI::KeyCode code, MyGUI::Char ch) { - if (code == MyGUI::KeyCode::None) - { - propagateEvent("textInput", [ch](auto w) { - MyGUI::UString uString; - uString.push_back(static_cast(ch)); - return sol::make_object(w->lua(), uString.asUTF8()); - }); - } - else - propagateEvent("keyPress", [code](auto w) { return w->keyEvent(code); }); + protectedCall([=, this](LuaUtil::LuaView& view) { + if (code == MyGUI::KeyCode::None) + { + propagateEvent("textInput", [&](auto w) { + MyGUI::UString uString; + uString.push_back(static_cast(ch)); + return sol::make_object(view.sol(), uString.asUTF8()); + }); + } + else + propagateEvent("keyPress", [&](auto w) { return w->keyEvent(view, code); }); + }); } void WidgetExtension::keyRelease(MyGUI::Widget*, MyGUI::KeyCode code) { - propagateEvent("keyRelease", [code](auto w) { return w->keyEvent(code); }); + protectedCall([=, this](LuaUtil::LuaView& view) { + propagateEvent("keyRelease", [&](auto w) { return w->keyEvent(view, code); }); + }); } void WidgetExtension::mouseMove(MyGUI::Widget*, int left, int top) { - propagateEvent("mouseMove", [left, top](auto w) { return w->mouseEvent(left, top); }); + protectedCall([=, this](LuaUtil::LuaView& view) { + propagateEvent("mouseMove", [&](auto w) { return w->mouseEvent(view, left, top); }); + }); } void WidgetExtension::mouseDrag(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) { - propagateEvent("mouseMove", [left, top, button](auto w) { return w->mouseEvent(left, top, button); }); + protectedCall([=, this](LuaUtil::LuaView& view) { + propagateEvent("mouseMove", [&](auto w) { return w->mouseEvent(view, left, top, button); }); + }); } void WidgetExtension::mouseClick(MyGUI::Widget* /*widget*/) @@ -412,12 +415,16 @@ namespace LuaUi void WidgetExtension::mousePress(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) { - propagateEvent("mousePress", [left, top, button](auto w) { return w->mouseEvent(left, top, button); }); + protectedCall([=, this](LuaUtil::LuaView& view) { + propagateEvent("mousePress", [&](auto w) { return w->mouseEvent(view, left, top, button); }); + }); } void WidgetExtension::mouseRelease(MyGUI::Widget*, int left, int top, MyGUI::MouseButton button) { - propagateEvent("mouseRelease", [left, top, button](auto w) { return w->mouseEvent(left, top, button); }); + protectedCall([=, this](LuaUtil::LuaView& view) { + propagateEvent("mouseRelease", [&](auto w) { return w->mouseEvent(view, left, top, button); }); + }); } void WidgetExtension::focusGain(MyGUI::Widget*, MyGUI::Widget*) diff --git a/components/lua_ui/widget.hpp b/components/lua_ui/widget.hpp index 5fcf86d110..c20f70587e 100644 --- a/components/lua_ui/widget.hpp +++ b/components/lua_ui/widget.hpp @@ -83,9 +83,8 @@ namespace LuaUi void registerEvents(MyGUI::Widget* w); void clearEvents(MyGUI::Widget* w); - sol::table makeTable() const; - sol::object keyEvent(MyGUI::KeyCode) const; - sol::object mouseEvent(int left, int top, MyGUI::MouseButton button) const; + sol::object keyEvent(LuaUtil::LuaView& view, MyGUI::KeyCode) const; + sol::object mouseEvent(LuaUtil::LuaView& view, int left, int top, MyGUI::MouseButton button) const; MyGUI::IntSize parentSize() const; virtual MyGUI::IntSize childScalingSize() const; @@ -104,7 +103,11 @@ namespace LuaUi virtual void updateProperties(); virtual void updateChildren() {} - lua_State* lua() const { return mLua; } + template + void protectedCall(Lambda&& f) const + { + LuaUtil::protectedCall(mLua, std::forward(f)); + } void triggerEvent(std::string_view name, sol::object argument) const; template diff --git a/components/lua_ui/window.cpp b/components/lua_ui/window.cpp index 3331dd47b1..d86cbf9cb4 100644 --- a/components/lua_ui/window.cpp +++ b/components/lua_ui/window.cpp @@ -78,9 +78,11 @@ namespace LuaUi mPreviousMouse.left = left; mPreviousMouse.top = top; - sol::table table = makeTable(); - table["position"] = osg::Vec2f(static_cast(mCoord.left), static_cast(mCoord.top)); - table["size"] = osg::Vec2f(static_cast(mCoord.width), static_cast(mCoord.height)); - triggerEvent("windowDrag", table); + protectedCall([this](LuaUtil::LuaView& view) { + sol::table table = view.newTable(); + table["position"] = osg::Vec2f(static_cast(mCoord.left), static_cast(mCoord.top)); + table["size"] = osg::Vec2f(static_cast(mCoord.width), static_cast(mCoord.height)); + triggerEvent("windowDrag", table); + }); } } diff --git a/components/misc/messageformatparser.cpp b/components/misc/messageformatparser.cpp index 649d0184d3..67ac07011c 100644 --- a/components/misc/messageformatparser.cpp +++ b/components/misc/messageformatparser.cpp @@ -27,58 +27,71 @@ namespace Misc { for (std::size_t i = 0; i < m.size(); ++i) { - if (m[i] == '%') - { - if (++i < m.size()) - { - if (m[i] == '%') - visitedCharacter('%'); - else - { - char pad = ' '; - if (m[i] == '0' || m[i] == ' ') - { - pad = m[i]; - ++i; - } - - int width = parseNumber(i, m, -1); - - if (i < m.size()) - { - int precision = -1; - if (m[i] == '.') - { - ++i; - precision = parseNumber(i, m, 0); - } - - if (i < m.size()) - { - if (m[i] == 'S' || m[i] == 's') - visitedPlaceholder(StringPlaceholder, pad, width, precision, Notation::Fixed); - else if (m[i] == 'd' || m[i] == 'i') - visitedPlaceholder(IntegerPlaceholder, pad, width, precision, Notation::Fixed); - else if (m[i] == 'f' || m[i] == 'F') - visitedPlaceholder(FloatPlaceholder, pad, width, precision, Notation::Fixed); - else if (m[i] == 'e' || m[i] == 'E') - visitedPlaceholder(FloatPlaceholder, pad, width, precision, Notation::Scientific); - else if (m[i] == 'g' || m[i] == 'G') - visitedPlaceholder(FloatPlaceholder, pad, width, precision, Notation::Shortest); - else if (m[i] == 'a') - visitedPlaceholder(FloatPlaceholder, pad, width, precision, Notation::HexLower); - else if (m[i] == 'A') - visitedPlaceholder(FloatPlaceholder, pad, width, precision, Notation::HexUpper); - else - visitedCharacter(m[i]); - } - } - } - } - } - else + if (m[i] != '%') { visitedCharacter(m[i]); + continue; + } + if (++i == m.size()) + break; + if (m[i] == '%') + { + visitedCharacter('%'); + continue; + } + + int flags = None; + while (i < m.size()) + { + if (m[i] == '-') + flags |= AlignLeft; + else if (m[i] == '+') + flags |= PositiveSign; + else if (m[i] == ' ') + flags |= PositiveSpace; + else if (m[i] == '0') + flags |= PrependZero; + else if (m[i] == '#') + flags |= AlternateForm; + else + break; + ++i; + } + + int width = parseNumber(i, m, -1); + + if (i < m.size()) + { + int precision = -1; + if (m[i] == '.') + { + ++i; + precision = parseNumber(i, m, 0); + } + + if (i < m.size()) + { + if (m[i] == 'S' || m[i] == 's') + visitedPlaceholder(StringPlaceholder, flags, width, precision, Notation::Fixed); + else if (m[i] == 'd' || m[i] == 'i') + visitedPlaceholder(IntegerPlaceholder, flags, width, precision, Notation::Fixed); + else if (m[i] == 'f' || m[i] == 'F') + visitedPlaceholder(FloatPlaceholder, flags, width, precision, Notation::Fixed); + else if (m[i] == 'e') + visitedPlaceholder(FloatPlaceholder, flags, width, precision, Notation::ScientificLower); + else if (m[i] == 'E') + visitedPlaceholder(FloatPlaceholder, flags, width, precision, Notation::ScientificUpper); + else if (m[i] == 'g') + visitedPlaceholder(FloatPlaceholder, flags, width, precision, Notation::ShortestLower); + else if (m[i] == 'G') + visitedPlaceholder(FloatPlaceholder, flags, width, precision, Notation::ShortestUpper); + else if (m[i] == 'a') + visitedPlaceholder(FloatPlaceholder, flags, width, precision, Notation::HexLower); + else if (m[i] == 'A') + visitedPlaceholder(FloatPlaceholder, flags, width, precision, Notation::HexUpper); + else + visitedCharacter(m[i]); + } } } } diff --git a/components/misc/messageformatparser.hpp b/components/misc/messageformatparser.hpp index eeef29234d..99a42d2739 100644 --- a/components/misc/messageformatparser.hpp +++ b/components/misc/messageformatparser.hpp @@ -15,17 +15,28 @@ namespace Misc FloatPlaceholder }; - enum class Notation + enum Flags { - Fixed, - Scientific, - Shortest, - HexUpper, - HexLower + None = 0, + PositiveSpace = 1, + PositiveSign = 2, + AlignLeft = 4, + PrependZero = 8, + AlternateForm = 16 }; - virtual void visitedPlaceholder( - Placeholder placeholder, char padding, int width, int precision, Notation notation) + enum class Notation : char + { + Fixed = 'f', + ScientificUpper = 'E', + ScientificLower = 'e', + ShortestUpper = 'G', + ShortestLower = 'g', + HexUpper = 'A', + HexLower = 'a' + }; + + virtual void visitedPlaceholder(Placeholder placeholder, int flags, int width, int precision, Notation notation) = 0; virtual void visitedCharacter(char c) = 0; diff --git a/components/nif/physics.cpp b/components/nif/physics.cpp index 6368537e60..f855edb30a 100644 --- a/components/nif/physics.cpp +++ b/components/nif/physics.cpp @@ -770,6 +770,11 @@ namespace Nif filter.read(nif); } + void bhkListShape::post(Reader& nif) + { + postRecordList(nif, mSubshapes); + } + void bhkCompressedMeshShape::read(NIFStream* nif) { mTarget.read(nif); diff --git a/components/nif/physics.hpp b/components/nif/physics.hpp index 286c8cc353..d3ac22b4b8 100644 --- a/components/nif/physics.hpp +++ b/components/nif/physics.hpp @@ -730,6 +730,7 @@ namespace Nif std::vector mHavokFilters; void read(NIFStream* nif) override; + void post(Reader& nif) override; }; struct bhkCompressedMeshShape : public bhkShape diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst index 810739caeb..b49e293744 100644 --- a/docs/source/reference/lua-scripting/events.rst +++ b/docs/source/reference/lua-scripting/events.rst @@ -218,7 +218,7 @@ Lock a container or door .. code-block:: Lua - core.sendGlobalEvent('Lock', {taret = selected, magnitude = 50}) + core.sendGlobalEvent('Lock', {target = selected, magnitude = 50}) **Unlock** @@ -226,4 +226,4 @@ Unlock a container or door .. code-block:: Lua - core.sendGlobalEvent('Unlock', {taret = selected}) + core.sendGlobalEvent('Unlock', {target = selected}) diff --git a/files/data/scripts/omw/mechanics/playercontroller.lua b/files/data/scripts/omw/mechanics/playercontroller.lua index a0d1b11362..8345ed2166 100644 --- a/files/data/scripts/omw/mechanics/playercontroller.lua +++ b/files/data/scripts/omw/mechanics/playercontroller.lua @@ -132,6 +132,12 @@ local function skillUsedHandler(skillid, params) end local skillStat = NPC.stats.skills[skillid](self) + + if (skillStat.base >= 100 and params.skillGain > 0) or + (skillStat.base <= 0 and params.skillGain < 0) then + return false + end + skillStat.progress = skillStat.progress + params.skillGain / I.SkillProgression.getSkillProgressRequirement(skillid) if skillStat.progress >= 1 then diff --git a/files/data/scripts/omw/mwui/constants.lua b/files/data/scripts/omw/mwui/constants.lua index 8a02268f77..918a09376b 100644 --- a/files/data/scripts/omw/mwui/constants.lua +++ b/files/data/scripts/omw/mwui/constants.lua @@ -1,11 +1,12 @@ +local core = require('openmw.core') local ui = require('openmw.ui') local util = require('openmw.util') return { - textNormalSize = 16, - textHeaderSize = 16, - headerColor = util.color.rgb(223 / 255, 201 / 255, 159 / 255), - normalColor = util.color.rgb(202 / 255, 165 / 255, 96 / 255), + textNormalSize = ui._getDefaultFontSize(), + textHeaderSize = ui._getDefaultFontSize(), + headerColor = util.color.commaString(core.getGMST("FontColor_color_header")), + normalColor = util.color.commaString(core.getGMST("FontColor_color_normal")), border = 2, thickBorder = 4, padding = 2, diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 545cda634f..e5d5a3ae7f 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -57,10 +57,13 @@ -- @return #number --- --- Get a GMST setting from content files. +-- Get a game setting with given name (from GMST ESM records or from openmw.cfg). -- @function [parent=#core] getGMST -- @param #string setting Setting name -- @return #any +-- @usage local skillBonus = core.getGMST('fMinorSkillBonus') -- get a numeric GMST from ESM data +-- @usage local jailFormatString = core.getGMST('sNotifyMessage42') -- get a string GMST from ESM data +-- @usage local bloodTextureName = core.getGMST('Blood_Texture_1') -- get a "fallback" parameter value from openmw.cfg (always a string) --- -- The game's difficulty setting. diff --git a/files/lua_api/openmw/util.lua b/files/lua_api/openmw/util.lua index 0ba1fe2a8e..f04b24843d 100644 --- a/files/lua_api/openmw/util.lua +++ b/files/lua_api/openmw/util.lua @@ -477,6 +477,16 @@ -- @param #number a -- @return #Color +--- +-- Creates a Color from comma-separated string (in RGB or RGBA order, spaces are ignored) +-- @function [parent=#COLOR] commaString +-- @param #string str +-- @return #Color +-- @usage local color = util.color.commaString('255,0,0') -- red color +-- @usage local color = util.color.commaString('10000,0,0') -- red color (values are still capped at 255) +-- @usage local color = util.color.commaString('0, 0, 255, 255') -- blue color, with explicit alpha +-- @usage local color = util.color.commaString('0,255,0,128') -- green color, semi-transparent + --- -- Creates a Color from RGB format. Equivalent to calling util.rgba with a = 1. -- @function [parent=#COLOR] rgb