Merge branch 'menuscripts' into 'master'

Add new Lua context: menu scripts

Closes #7805 and #7648

See merge request OpenMW/openmw!3464
ini_importer_tests
Zackhasacat 11 months ago
commit 1338e884a9

@ -174,11 +174,13 @@
Feature #7618: Show the player character's health in the save details
Feature #7625: Add some missing console error outputs
Feature #7634: Support NiParticleBomb
Feature #7648: Lua Save game API
Feature #7652: Sort inactive post processing shaders list properly
Feature #7698: Implement sAbsorb, sDamage, sDrain, sFortify and sRestore
Feature #7709: Improve resolution selection in Launcher
Feature #7792: Support Timescale Clouds
Feature #7795: Support MaxNumberRipples INI setting
Feature #7805: Lua Menu context
Task #5896: Do not use deprecated MyGUI properties
Task #6624: Drop support for saves made prior to 0.45
Task #7113: Move from std::atoi to std::from_char

@ -72,7 +72,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 51)
set(OPENMW_LUA_API_REVISION 52)
set(OPENMW_POSTPROCESSING_API_REVISION 1)
set(OPENMW_VERSION_COMMITHASH "")

@ -61,10 +61,14 @@ add_openmw_dir (mwscript
add_openmw_dir (mwlua
luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant
context globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings
camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings itemdata
types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist types/terminal
worker magicbindings factionbindings classbindings animationbindings
context menuscripts globalscripts localscripts playerscripts luabindings objectbindings cellbindings
mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings
postprocessingbindings stats debugbindings corebindings worldbindings worker magicbindings factionbindings
classbindings itemdata inputprocessor animationbindings
types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc
types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus
types/potion types/ingredient types/misc types/repair types/armor types/light types/static
types/clothing types/levelledlist types/terminal
)
add_openmw_dir (mwsound

@ -890,8 +890,8 @@ void OMW::Engine::prepareEngine()
<< 100 * static_cast<double>(result.second) / result.first << "%)";
}
mLuaManager->init();
mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath());
mLuaManager->init();
// starts a separate lua thread if "lua num threads" > 0
mLuaWorker = std::make_unique<MWLua::Worker>(*mLuaManager, *mViewer);

@ -55,6 +55,7 @@ namespace MWBase
virtual void newGameStarted() = 0;
virtual void gameLoaded() = 0;
virtual void gameEnded() = 0;
virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0;
virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0;
virtual void objectTeleported(const MWWorld::Ptr& ptr) = 0;

@ -44,6 +44,9 @@ namespace MWBase
virtual void askLoadRecent() = 0;
virtual void requestNewGame() = 0;
virtual void requestLoad(const std::filesystem::path& filepath) = 0;
virtual State getState() const = 0;
virtual void newGame(bool bypass = false) = 0;

@ -166,7 +166,8 @@ namespace MWBase
virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0;
virtual MWWorld::Ptr getConsoleSelectedObject() const = 0;
virtual void setConsoleMode(const std::string& mode) = 0;
virtual void setConsoleMode(std::string_view mode) = 0;
virtual const std::string& getConsoleMode() = 0;
static constexpr std::string_view sConsoleColor_Default = "#FFFFFF";
static constexpr std::string_view sConsoleColor_Error = "#FF2222";

@ -51,6 +51,7 @@
#include <components/l10n/manager.hpp>
#include <components/lua_ui/util.hpp>
#include <components/lua_ui/widget.hpp>
#include <components/settings/values.hpp>
@ -546,7 +547,8 @@ namespace MWGui
{
try
{
LuaUi::clearUserInterface();
LuaUi::clearGameInterface();
LuaUi::clearMenuInterface();
mStatsWatcher.reset();
@ -1675,7 +1677,10 @@ namespace MWGui
void WindowManager::onKeyFocusChanged(MyGUI::Widget* widget)
{
if (widget && widget->castType<MyGUI::EditBox>(false))
bool isEditBox = widget && widget->castType<MyGUI::EditBox>(false);
LuaUi::WidgetExtension* luaWidget = dynamic_cast<LuaUi::WidgetExtension*>(widget);
bool capturesInput = luaWidget ? luaWidget->isTextInput() : isEditBox;
if (widget && capturesInput)
SDL_StartTextInput();
else
SDL_StopTextInput();
@ -2173,11 +2178,16 @@ namespace MWGui
mConsole->print(msg, color);
}
void WindowManager::setConsoleMode(const std::string& mode)
void WindowManager::setConsoleMode(std::string_view mode)
{
mConsole->setConsoleMode(mode);
}
const std::string& WindowManager::getConsoleMode()
{
return mConsole->getConsoleMode();
}
void WindowManager::createCursors()
{
MyGUI::ResourceManager::EnumeratorPtr enumerator = MyGUI::ResourceManager::getInstance().getEnumerator();

@ -192,7 +192,8 @@ namespace MWGui
void setConsoleSelectedObject(const MWWorld::Ptr& object) override;
MWWorld::Ptr getConsoleSelectedObject() const override;
void printToConsole(const std::string& msg, std::string_view color) override;
void setConsoleMode(const std::string& mode) override;
void setConsoleMode(std::string_view mode) override;
const std::string& getConsoleMode() override;
/// Set time left for the player to start drowning (update the drowning bar)
/// @param time time left to start drowning

@ -15,6 +15,7 @@ namespace MWLua
struct Context
{
bool mIsMenu;
bool mIsGlobal;
LuaManager* mLuaManager;
LuaUtil::LuaState* mLua;

@ -0,0 +1,141 @@
#include "corebindings.hpp"
#include <chrono>
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadfact.hpp>
#include <components/lua/l10n.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/serialization.hpp>
#include <components/misc/strings/algorithm.hpp>
#include <components/misc/strings/lower.hpp>
#include <components/version/version.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/datetimemanager.hpp"
#include "../mwworld/esmstore.hpp"
#include "animationbindings.hpp"
#include "factionbindings.hpp"
#include "luaevents.hpp"
#include "magicbindings.hpp"
#include "soundbindings.hpp"
#include "stats.hpp"
namespace MWLua
{
static sol::table initContentFilesBindings(sol::state_view& lua)
{
const std::vector<std::string>& contentList = MWBase::Environment::get().getWorld()->getContentFiles();
sol::table list(lua, sol::create);
for (size_t i = 0; i < contentList.size(); ++i)
list[i + 1] = Misc::StringUtils::lowerCase(contentList[i]);
sol::table res(lua, sol::create);
res["list"] = LuaUtil::makeReadOnly(list);
res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional<int> {
for (size_t i = 0; i < contentList.size(); ++i)
if (Misc::StringUtils::ciEqual(contentList[i], contentFile))
return i + 1;
return sol::nullopt;
};
res["has"] = [&contentList](std::string_view contentFile) -> bool {
for (size_t i = 0; i < contentList.size(); ++i)
if (Misc::StringUtils::ciEqual(contentList[i], contentFile))
return true;
return false;
};
return LuaUtil::makeReadOnly(res);
}
void addCoreTimeBindings(sol::table& api, const Context& context)
{
MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager();
api["getSimulationTime"] = [timeManager]() { return timeManager->getSimulationTime(); };
api["getSimulationTimeScale"] = [timeManager]() { return timeManager->getSimulationTimeScale(); };
api["getGameTime"] = [timeManager]() { return timeManager->getGameTime(); };
api["getGameTimeScale"] = [timeManager]() { return timeManager->getGameTimeScale(); };
api["isWorldPaused"] = [timeManager]() { return timeManager->isPaused(); };
api["getRealTime"] = []() {
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
};
// TODO: remove in global context?
api["getRealFrameDuration"] = []() { return MWBase::Environment::get().getFrameDuration(); };
}
sol::table initCorePackage(const Context& context)
{
auto* lua = context.mLua;
if (lua->sol()["openmw_core"] != sol::nil)
return lua->sol()["openmw_core"];
sol::table api(lua->sol(), sol::create);
api["API_REVISION"] = Version::getLuaApiRevision(); // specified in CMakeLists.txt
api["quit"] = [lua]() {
Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback();
MWBase::Environment::get().getStateManager()->requestQuit();
};
api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) {
context.mLuaEvents->addGlobalEvent(
{ std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) });
};
api["contentFiles"] = initContentFilesBindings(lua->sol());
api["sound"] = initCoreSoundBindings(context);
api["vfx"] = initCoreVfxBindings(context);
api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string {
const std::vector<std::string>& contentList = MWBase::Environment::get().getWorld()->getContentFiles();
for (size_t i = 0; i < contentList.size(); ++i)
if (Misc::StringUtils::ciEqual(contentList[i], contentFile))
return ESM::RefId(ESM::FormId{ index, int(i) }).serializeText();
throw std::runtime_error("Content file not found: " + std::string(contentFile));
};
addCoreTimeBindings(api, context);
api["magic"] = initCoreMagicBindings(context);
api["stats"] = initCoreStatsBindings(context);
initCoreFactionBindings(context);
api["factions"] = &MWBase::Environment::get().getESMStore()->get<ESM::Faction>();
api["l10n"] = LuaUtil::initL10nLoader(lua->sol(), MWBase::Environment::get().getL10nManager());
const MWWorld::Store<ESM::GameSetting>* gmstStore
= &MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
api["getGMST"] = [lua = context.mLua, gmstStore](const std::string& setting) -> sol::object {
const ESM::GameSetting* gmst = gmstStore->search(setting);
if (gmst == nullptr)
return sol::nil;
const ESM::Variant& value = gmst->mValue;
switch (value.getType())
{
case ESM::VT_Float:
return sol::make_object<float>(lua->sol(), value.getFloat());
case ESM::VT_Short:
case ESM::VT_Long:
case ESM::VT_Int:
return sol::make_object<int>(lua->sol(), value.getInteger());
case ESM::VT_String:
return sol::make_object<std::string>(lua->sol(), value.getString());
case ESM::VT_Unknown:
case ESM::VT_None:
break;
}
return sol::nil;
};
lua->sol()["openmw_core"] = LuaUtil::makeReadOnly(api);
return lua->sol()["openmw_core"];
}
sol::table initCorePackageForMenuScripts(const Context& context)
{
sol::table api(context.mLua->sol(), sol::create);
for (auto& [k, v] : LuaUtil::getMutableFromReadOnly(initCorePackage(context)))
api[k] = v;
api["sendGlobalEvent"] = sol::nil;
api["sound"] = sol::nil;
api["vfx"] = sol::nil;
return LuaUtil::makeReadOnly(api);
}
}

@ -0,0 +1,19 @@
#ifndef MWLUA_COREBINDINGS_H
#define MWLUA_COREBINDINGS_H
#include <sol/forward.hpp>
#include "context.hpp"
namespace MWLua
{
void addCoreTimeBindings(sol::table& api, const Context& context);
sol::table initCorePackage(const Context&);
// Returns `openmw.core`, but disables the functionality that shouldn't
// be availabe in menu scripts (to prevent cheating in mutiplayer via menu console).
sol::table initCorePackageForMenuScripts(const Context&);
}
#endif // MWLUA_COREBINDINGS_H

@ -1,10 +1,6 @@
#ifndef MWLUA_GLOBALSCRIPTS_H
#define MWLUA_GLOBALSCRIPTS_H
#include <memory>
#include <set>
#include <string>
#include <components/lua/luastate.hpp>
#include <components/lua/scriptscontainer.hpp>

@ -36,6 +36,12 @@ namespace MWLua
sol::table initInputPackage(const Context& context)
{
{
sol::state_view& lua = context.mLua->sol();
if (lua["openmw_input"] != sol::nil)
return lua["openmw_input"];
}
sol::usertype<SDL_Keysym> keyEvent = context.mLua->sol().new_usertype<SDL_Keysym>("KeyEvent");
keyEvent["symbol"] = sol::readonly_property([](const SDL_Keysym& e) {
if (e.sym > 0 && e.sym <= 255)
@ -424,7 +430,9 @@ namespace MWLua
{ "Tab", SDL_SCANCODE_TAB },
}));
return LuaUtil::makeReadOnly(api);
sol::state_view& lua = context.mLua->sol();
lua["openmw_input"] = LuaUtil::makeReadOnly(api);
return lua["openmw_input"];
}
}

@ -0,0 +1,72 @@
#ifndef MWLUA_INPUTPROCESSOR_H
#define MWLUA_INPUTPROCESSOR_H
#include <SDL_events.h>
#include <components/sdlutil/events.hpp>
#include "../mwbase/luamanager.hpp"
namespace MWLua
{
template <class Container>
class InputProcessor
{
public:
InputProcessor(Container* scriptsContainer)
: mScriptsContainer(scriptsContainer)
{
mScriptsContainer->registerEngineHandlers({ &mKeyPressHandlers, &mKeyReleaseHandlers,
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mTouchpadPressed,
&mTouchpadReleased, &mTouchpadMoved });
}
void processInputEvent(const MWBase::LuaManager::InputEvent& event)
{
using InputEvent = MWBase::LuaManager::InputEvent;
switch (event.mType)
{
case InputEvent::KeyPressed:
mScriptsContainer->callEngineHandlers(mKeyPressHandlers, std::get<SDL_Keysym>(event.mValue));
break;
case InputEvent::KeyReleased:
mScriptsContainer->callEngineHandlers(mKeyReleaseHandlers, std::get<SDL_Keysym>(event.mValue));
break;
case InputEvent::ControllerPressed:
mScriptsContainer->callEngineHandlers(mControllerButtonPressHandlers, std::get<int>(event.mValue));
break;
case InputEvent::ControllerReleased:
mScriptsContainer->callEngineHandlers(
mControllerButtonReleaseHandlers, std::get<int>(event.mValue));
break;
case InputEvent::Action:
mScriptsContainer->callEngineHandlers(mActionHandlers, std::get<int>(event.mValue));
break;
case InputEvent::TouchPressed:
mScriptsContainer->callEngineHandlers(
mTouchpadPressed, std::get<SDLUtil::TouchEvent>(event.mValue));
break;
case InputEvent::TouchReleased:
mScriptsContainer->callEngineHandlers(
mTouchpadReleased, std::get<SDLUtil::TouchEvent>(event.mValue));
break;
case InputEvent::TouchMoved:
mScriptsContainer->callEngineHandlers(mTouchpadMoved, std::get<SDLUtil::TouchEvent>(event.mValue));
break;
}
}
private:
Container* mScriptsContainer;
typename Container::EngineHandlerList mKeyPressHandlers{ "onKeyPress" };
typename Container::EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" };
typename Container::EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" };
typename Container::EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" };
typename Container::EngineHandlerList mActionHandlers{ "onInputAction" };
typename Container::EngineHandlerList mTouchpadPressed{ "onTouchPress" };
typename Container::EngineHandlerList mTouchpadReleased{ "onTouchRelease" };
typename Container::EngineHandlerList mTouchpadMoved{ "onTouchMove" };
};
}
#endif // MWLUA_INPUTPROCESSOR_H

@ -1,332 +1,31 @@
#include "luabindings.hpp"
#include <chrono>
#include <components/esm/attr.hpp>
#include <components/esm3/loadacti.hpp>
#include <components/esm3/loadalch.hpp>
#include <components/esm3/loadarmo.hpp>
#include <components/esm3/loadbook.hpp>
#include <components/esm3/loadclot.hpp>
#include <components/esm3/loadfact.hpp>
#include <components/esm3/loadmisc.hpp>
#include <components/esm3/loadskil.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/lua/l10n.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/asyncpackage.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/misc/strings/lower.hpp>
#include <components/version/version.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/action.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/datetimemanager.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/manualref.hpp"
#include "../mwworld/store.hpp"
#include "../mwworld/worldmodel.hpp"
#include "luaevents.hpp"
#include "luamanagerimp.hpp"
#include "mwscriptbindings.hpp"
#include "objectlists.hpp"
#include "animationbindings.hpp"
#include "camerabindings.hpp"
#include "cellbindings.hpp"
#include "corebindings.hpp"
#include "debugbindings.hpp"
#include "factionbindings.hpp"
#include "inputbindings.hpp"
#include "magicbindings.hpp"
#include "localscripts.hpp"
#include "menuscripts.hpp"
#include "nearbybindings.hpp"
#include "objectbindings.hpp"
#include "postprocessingbindings.hpp"
#include "soundbindings.hpp"
#include "stats.hpp"
#include "types/types.hpp"
#include "uibindings.hpp"
#include "vfsbindings.hpp"
#include "worldbindings.hpp"
namespace MWLua
{
struct CellsStore
{
};
}
namespace sol
{
template <>
struct is_automagical<MWLua::CellsStore> : std::false_type
{
};
}
namespace MWLua
{
static void checkGameInitialized(LuaUtil::LuaState* lua)
{
if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame)
throw std::runtime_error(
"This function cannot be used until the game is fully initialized.\n" + lua->debugTraceback());
}
static void addTimeBindings(sol::table& api, const Context& context, bool global)
{
MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager();
api["getSimulationTime"] = [timeManager]() { return timeManager->getSimulationTime(); };
api["getSimulationTimeScale"] = [timeManager]() { return timeManager->getSimulationTimeScale(); };
api["getGameTime"] = [timeManager]() { return timeManager->getGameTime(); };
api["getGameTimeScale"] = [timeManager]() { return timeManager->getGameTimeScale(); };
api["isWorldPaused"] = [timeManager]() { return timeManager->isPaused(); };
api["getRealTime"] = []() {
return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
};
api["getRealFrameDuration"] = []() { return MWBase::Environment::get().getFrameDuration(); };
if (!global)
return;
api["setGameTimeScale"] = [timeManager](double scale) { timeManager->setGameTimeScale(scale); };
api["setSimulationTimeScale"] = [context, timeManager](float scale) {
context.mLuaManager->addAction([scale, timeManager] { timeManager->setSimulationTimeScale(scale); });
};
api["pause"]
= [timeManager](sol::optional<std::string_view> tag) { timeManager->pause(tag.value_or("paused")); };
api["unpause"]
= [timeManager](sol::optional<std::string_view> tag) { timeManager->unpause(tag.value_or("paused")); };
api["getPausedTags"] = [timeManager](sol::this_state lua) {
sol::table res(lua, sol::create);
for (const std::string& tag : timeManager->getPausedTags())
res[tag] = tag;
return res;
};
}
static sol::table initContentFilesBindings(sol::state_view& lua)
{
const std::vector<std::string>& contentList = MWBase::Environment::get().getWorld()->getContentFiles();
sol::table list(lua, sol::create);
for (size_t i = 0; i < contentList.size(); ++i)
list[i + 1] = Misc::StringUtils::lowerCase(contentList[i]);
sol::table res(lua, sol::create);
res["list"] = LuaUtil::makeReadOnly(list);
res["indexOf"] = [&contentList](std::string_view contentFile) -> sol::optional<int> {
for (size_t i = 0; i < contentList.size(); ++i)
if (Misc::StringUtils::ciEqual(contentList[i], contentFile))
return i + 1;
return sol::nullopt;
};
res["has"] = [&contentList](std::string_view contentFile) -> bool {
for (size_t i = 0; i < contentList.size(); ++i)
if (Misc::StringUtils::ciEqual(contentList[i], contentFile))
return true;
return false;
};
return LuaUtil::makeReadOnly(res);
}
static sol::table initCorePackage(const Context& context)
{
auto* lua = context.mLua;
sol::table api(lua->sol(), sol::create);
api["API_REVISION"] = Version::getLuaApiRevision(); // specified in CMakeLists.txt
api["quit"] = [lua]() {
Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback();
MWBase::Environment::get().getStateManager()->requestQuit();
};
api["sendGlobalEvent"] = [context](std::string eventName, const sol::object& eventData) {
context.mLuaEvents->addGlobalEvent(
{ std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) });
};
api["contentFiles"] = initContentFilesBindings(lua->sol());
api["sound"] = initCoreSoundBindings(context);
api["vfx"] = initCoreVfxBindings(context);
api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string {
const std::vector<std::string>& contentList = MWBase::Environment::get().getWorld()->getContentFiles();
for (size_t i = 0; i < contentList.size(); ++i)
if (Misc::StringUtils::ciEqual(contentList[i], contentFile))
return ESM::RefId(ESM::FormId{ index, int(i) }).serializeText();
throw std::runtime_error("Content file not found: " + std::string(contentFile));
};
addTimeBindings(api, context, false);
api["magic"] = initCoreMagicBindings(context);
api["stats"] = initCoreStatsBindings(context);
initCoreFactionBindings(context);
api["factions"] = &MWBase::Environment::get().getWorld()->getStore().get<ESM::Faction>();
api["l10n"] = LuaUtil::initL10nLoader(lua->sol(), MWBase::Environment::get().getL10nManager());
const MWWorld::Store<ESM::GameSetting>* gmstStore
= &MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
api["getGMST"] = [lua = context.mLua, gmstStore](const std::string& setting) -> sol::object {
const ESM::GameSetting* gmst = gmstStore->search(setting);
if (gmst == nullptr)
return sol::nil;
const ESM::Variant& value = gmst->mValue;
switch (value.getType())
{
case ESM::VT_Float:
return sol::make_object<float>(lua->sol(), value.getFloat());
case ESM::VT_Short:
case ESM::VT_Long:
case ESM::VT_Int:
return sol::make_object<int>(lua->sol(), value.getInteger());
case ESM::VT_String:
return sol::make_object<std::string>(lua->sol(), value.getString());
case ESM::VT_Unknown:
case ESM::VT_None:
break;
}
return sol::nil;
};
return LuaUtil::makeReadOnly(api);
}
static void addCellGetters(sol::table& api, const Context& context)
{
api["getCellByName"] = [](std::string_view name) {
return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(name, /*forceLoad=*/false) };
};
api["getExteriorCell"] = [](int x, int y, sol::object cellOrName) {
ESM::RefId worldspace;
if (cellOrName.is<GCell>())
worldspace = cellOrName.as<GCell>().mStore->getCell()->getWorldSpace();
else if (cellOrName.is<std::string_view>() && !cellOrName.as<std::string_view>().empty())
worldspace = MWBase::Environment::get()
.getWorldModel()
->getCell(cellOrName.as<std::string_view>())
.getCell()
->getWorldSpace();
else
worldspace = ESM::Cell::sDefaultWorldspaceId;
return GCell{ &MWBase::Environment::get().getWorldModel()->getExterior(
ESM::ExteriorCellLocation(x, y, worldspace), /*forceLoad=*/false) };
};
const MWWorld::Store<ESM::Cell>* cells3Store = &MWBase::Environment::get().getESMStore()->get<ESM::Cell>();
const MWWorld::Store<ESM4::Cell>* cells4Store = &MWBase::Environment::get().getESMStore()->get<ESM4::Cell>();
sol::usertype<CellsStore> cells = context.mLua->sol().new_usertype<CellsStore>("Cells");
cells[sol::meta_function::length]
= [cells3Store, cells4Store](const CellsStore&) { return cells3Store->getSize() + cells4Store->getSize(); };
cells[sol::meta_function::index]
= [cells3Store, cells4Store](const CellsStore&, size_t index) -> sol::optional<GCell> {
if (index > cells3Store->getSize() + cells3Store->getSize() || index == 0)
return sol::nullopt;
index--; // Translate from Lua's 1-based indexing.
if (index < cells3Store->getSize())
{
const ESM::Cell* cellRecord = cells3Store->at(index);
return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(
cellRecord->mId, /*forceLoad=*/false) };
}
else
{
const ESM4::Cell* cellRecord = cells4Store->at(index - cells3Store->getSize());
return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(
cellRecord->mId, /*forceLoad=*/false) };
}
};
cells[sol::meta_function::pairs] = context.mLua->sol()["ipairsForArray"].template get<sol::function>();
cells[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get<sol::function>();
api["cells"] = CellsStore{};
}
static sol::table initWorldPackage(const Context& context)
{
sol::table api(context.mLua->sol(), sol::create);
ObjectLists* objectLists = context.mObjectLists;
addTimeBindings(api, context, true);
addCellGetters(api, context);
api["mwscript"] = initMWScriptBindings(context);
api["activeActors"] = GObjectList{ objectLists->getActorsInScene() };
api["players"] = GObjectList{ objectLists->getPlayers() };
api["createObject"] = [lua = context.mLua](std::string_view recordId, sol::optional<int> count) -> GObject {
checkGameInitialized(lua);
MWWorld::ManualRef mref(*MWBase::Environment::get().getESMStore(), ESM::RefId::deserializeText(recordId));
const MWWorld::Ptr& ptr = mref.getPtr();
ptr.getRefData().disable();
MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getDraftCell();
MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1));
return GObject(newPtr);
};
api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject {
ESM::RefId refId = ESM::RefId::deserializeText(formIdStr);
if (!refId.is<ESM::FormId>())
throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId");
return GObject(*refId.getIf<ESM::FormId>());
};
// Creates a new record in the world database.
api["createRecord"] = sol::overload(
[lua = context.mLua](const ESM::Activator& activator) -> const ESM::Activator* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(activator);
},
[lua = context.mLua](const ESM::Armor& armor) -> const ESM::Armor* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(armor);
},
[lua = context.mLua](const ESM::Clothing& clothing) -> const ESM::Clothing* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(clothing);
},
[lua = context.mLua](const ESM::Book& book) -> const ESM::Book* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(book);
},
[lua = context.mLua](const ESM::Miscellaneous& misc) -> const ESM::Miscellaneous* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(misc);
},
[lua = context.mLua](const ESM::Potion& potion) -> const ESM::Potion* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(potion);
},
[lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(weapon);
});
api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) {
if (!object.ptr().getRefData().activate())
return;
context.mLuaManager->addAction(
[object, actor] {
const MWWorld::Ptr& objPtr = object.ptr();
const MWWorld::Ptr& actorPtr = actor.ptr();
objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr);
},
"_runStandardActivationAction");
};
api["_runStandardUseAction"] = [context](const GObject& object, const GObject& actor, bool force) {
context.mLuaManager->addAction(
[object, actor, force] {
const MWWorld::Ptr& actorPtr = actor.ptr();
const MWWorld::Ptr& objectPtr = object.ptr();
if (actorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr())
MWBase::Environment::get().getWindowManager()->useItem(objectPtr, force);
else
{
std::unique_ptr<MWWorld::Action> action = objectPtr.getClass().use(objectPtr, force);
action->execute(actorPtr, true);
}
},
"_runStandardUseAction");
};
return LuaUtil::makeReadOnly(api);
}
std::map<std::string, sol::object> initCommonPackages(const Context& context)
{
sol::state_view lua = context.mLua->sol();
@ -336,8 +35,6 @@ namespace MWLua
{ "openmw.async",
LuaUtil::getAsyncPackageInitializer(
lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) },
{ "openmw.core", initCorePackage(context) },
{ "openmw.types", initTypesPackage(context) },
{ "openmw.util", LuaUtil::initUtilPackage(lua) },
{ "openmw.vfs", initVFSPackage(context) },
};
@ -348,6 +45,8 @@ namespace MWLua
initObjectBindingsForGlobalScripts(context);
initCellBindingsForGlobalScripts(context);
return {
{ "openmw.core", initCorePackage(context) },
{ "openmw.types", initTypesPackage(context) },
{ "openmw.world", initWorldPackage(context) },
};
}
@ -358,6 +57,8 @@ namespace MWLua
initCellBindingsForLocalScripts(context);
LocalScripts::initializeSelfPackage(context);
return {
{ "openmw.core", initCorePackage(context) },
{ "openmw.types", initTypesPackage(context) },
{ "openmw.nearby", initNearbyPackage(context) },
};
}
@ -374,4 +75,14 @@ namespace MWLua
};
}
std::map<std::string, sol::object> initMenuPackages(const Context& context)
{
return {
{ "openmw.core", initCorePackageForMenuScripts(context) },
{ "openmw.ambient", initAmbientPackage(context) },
{ "openmw.ui", initUserInterfacePackage(context) },
{ "openmw.menu", initMenuPackage(context) },
{ "openmw.input", initInputPackage(context) },
};
}
}

@ -12,14 +12,18 @@ namespace MWLua
// Initialize Lua packages that are available for all scripts.
std::map<std::string, sol::object> initCommonPackages(const Context&);
// Initialize Lua packages that are available only for global scripts.
// Initialize Lua packages that are available for global scripts (additionally to common packages).
std::map<std::string, sol::object> initGlobalPackages(const Context&);
// Initialize Lua packages that are available only for local scripts (including player scripts).
// Initialize Lua packages that are available for local scripts (additionally to common packages).
std::map<std::string, sol::object> initLocalPackages(const Context&);
// Initialize Lua packages that are available only for local scripts on the player.
// Initialize Lua packages that are available only for local scripts on the player (additionally to common and local
// packages).
std::map<std::string, sol::object> initPlayerPackages(const Context&);
// Initialize Lua packages that are available only for menu scripts (additionally to common packages).
std::map<std::string, sol::object> initMenuPackages(const Context&);
}
#endif // MWLUA_LUABINDINGS_H

@ -13,6 +13,7 @@
#include "globalscripts.hpp"
#include "localscripts.hpp"
#include "menuscripts.hpp"
namespace MWLua
{
@ -23,6 +24,7 @@ namespace MWLua
mLocalEventBatch.clear();
mNewGlobalEventBatch.clear();
mNewLocalEventBatch.clear();
mMenuEvents.clear();
}
void LuaEvents::finalizeEventBatch()
@ -51,6 +53,13 @@ namespace MWLua
mLocalEventBatch.clear();
}
void LuaEvents::callMenuEventHandlers()
{
for (const Global& e : mMenuEvents)
mMenuScripts.receiveEvent(e.mEventName, e.mEventData);
mMenuEvents.clear();
}
template <typename Event>
static void saveEvent(ESM::ESMWriter& esm, ESM::RefNum dest, const Event& event)
{

@ -23,12 +23,14 @@ namespace MWLua
{
class GlobalScripts;
class MenuScripts;
class LuaEvents
{
public:
explicit LuaEvents(GlobalScripts& globalScripts)
explicit LuaEvents(GlobalScripts& globalScripts, MenuScripts& menuScripts)
: mGlobalScripts(globalScripts)
, mMenuScripts(menuScripts)
{
}
@ -45,11 +47,13 @@ namespace MWLua
};
void addGlobalEvent(Global event) { mNewGlobalEventBatch.push_back(std::move(event)); }
void addMenuEvent(Global event) { mMenuEvents.push_back(std::move(event)); }
void addLocalEvent(Local event) { mNewLocalEventBatch.push_back(std::move(event)); }
void clear();
void finalizeEventBatch();
void callEventHandlers();
void callMenuEventHandlers();
void load(lua_State* lua, ESM::ESMReader& esm, const std::map<int, int>& contentFileMapping,
const LuaUtil::UserdataSerializer* serializer);
@ -57,10 +61,12 @@ namespace MWLua
private:
GlobalScripts& mGlobalScripts;
MenuScripts& mMenuScripts;
std::vector<Global> mNewGlobalEventBatch;
std::vector<Local> mNewLocalEventBatch;
std::vector<Global> mGlobalEventBatch;
std::vector<Local> mLocalEventBatch;
std::vector<Global> mMenuEvents;
};
}

@ -18,6 +18,7 @@
#include <components/l10n/manager.hpp>
#include <components/lua_ui/content.hpp>
#include <components/lua_ui/registerscriptsettings.hpp>
#include <components/lua_ui/util.hpp>
#include "../mwbase/windowmanager.hpp"
@ -65,18 +66,25 @@ namespace MWLua
mGlobalScripts.setSerializer(mGlobalSerializer.get());
}
LuaManager::~LuaManager()
{
LuaUi::clearSettings();
}
void LuaManager::initConfiguration()
{
mConfiguration.init(MWBase::Environment::get().getESMStore()->getLuaScriptsCfg());
Log(Debug::Verbose) << "Lua scripts configuration (" << mConfiguration.size() << " scripts):";
for (size_t i = 0; i < mConfiguration.size(); ++i)
Log(Debug::Verbose) << "#" << i << " " << LuaUtil::scriptCfgToString(mConfiguration[i]);
mMenuScripts.setAutoStartConf(mConfiguration.getMenuConf());
mGlobalScripts.setAutoStartConf(mConfiguration.getGlobalConf());
}
void LuaManager::init()
{
Context context;
context.mIsMenu = false;
context.mIsGlobal = true;
context.mLuaManager = this;
context.mLua = &mLua;
@ -88,28 +96,42 @@ namespace MWLua
localContext.mIsGlobal = false;
localContext.mSerializer = mLocalSerializer.get();
Context menuContext = context;
menuContext.mIsMenu = true;
for (const auto& [name, package] : initCommonPackages(context))
mLua.addCommonPackage(name, package);
for (const auto& [name, package] : initGlobalPackages(context))
mGlobalScripts.addPackage(name, package);
for (const auto& [name, package] : initMenuPackages(menuContext))
mMenuScripts.addPackage(name, package);
mLocalPackages = initLocalPackages(localContext);
mPlayerPackages = initPlayerPackages(localContext);
mPlayerPackages.insert(mLocalPackages.begin(), mLocalPackages.end());
LuaUtil::LuaStorage::initLuaBindings(mLua.sol());
mGlobalScripts.addPackage(
"openmw.storage", LuaUtil::LuaStorage::initGlobalPackage(mLua.sol(), &mGlobalStorage));
mMenuScripts.addPackage(
"openmw.storage", LuaUtil::LuaStorage::initMenuPackage(mLua.sol(), &mGlobalStorage, &mPlayerStorage));
mLocalPackages["openmw.storage"] = LuaUtil::LuaStorage::initLocalPackage(mLua.sol(), &mGlobalStorage);
mPlayerPackages["openmw.storage"]
= LuaUtil::LuaStorage::initPlayerPackage(mLua.sol(), &mGlobalStorage, &mPlayerStorage);
mPlayerStorage.setActive(true);
mGlobalStorage.setActive(false);
initConfiguration();
mInitialized = true;
mMenuScripts.addAutoStartedScripts();
}
void LuaManager::loadPermanentStorage(const std::filesystem::path& userConfigPath)
{
mPlayerStorage.setActive(true);
mGlobalStorage.setActive(true);
const auto globalPath = userConfigPath / "global_storage.bin";
const auto playerPath = userConfigPath / "player_storage.bin";
if (std::filesystem::exists(globalPath))
@ -120,7 +142,8 @@ namespace MWLua
void LuaManager::savePermanentStorage(const std::filesystem::path& userConfigPath)
{
mGlobalStorage.save(userConfigPath / "global_storage.bin");
if (mGlobalScriptsStarted)
mGlobalStorage.save(userConfigPath / "global_storage.bin");
mPlayerStorage.save(userConfigPath / "player_storage.bin");
}
@ -143,6 +166,10 @@ namespace MWLua
mObjectLists.update();
for (auto scripts : mQueuedAutoStartedScripts)
scripts->addAutoStartedScripts();
mQueuedAutoStartedScripts.clear();
std::erase_if(mActiveLocalScripts,
[](const LocalScripts* l) { return l->getPtrOrEmpty().isEmpty() || l->getPtrOrEmpty().mRef->isDeleted(); });
@ -206,9 +233,6 @@ namespace MWLua
void LuaManager::synchronizedUpdate()
{
if (mPlayer.isEmpty())
return; // The game is not started yet.
if (mNewGameStarted)
{
mNewGameStarted = false;
@ -219,18 +243,25 @@ namespace MWLua
// We apply input events in `synchronizedUpdate` rather than in `update` in order to reduce input latency.
mProcessingInputEvents = true;
PlayerScripts* playerScripts = dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
PlayerScripts* playerScripts
= mPlayer.isEmpty() ? nullptr : dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager();
for (const auto& event : mMenuInputEvents)
mMenuScripts.processInputEvent(event);
mMenuInputEvents.clear();
if (playerScripts && !windowManager->containsMode(MWGui::GM_MainMenu))
{
for (const auto& event : mInputEvents)
playerScripts->processInputEvent(event);
}
mInputEvents.clear();
mLuaEvents.callMenuEventHandlers();
double frameDuration = MWBase::Environment::get().getWorld()->getTimeManager()->isPaused()
? 0.0
: MWBase::Environment::get().getFrameDuration();
mInputActions.update(frameDuration);
mMenuScripts.onFrame(frameDuration);
if (playerScripts)
playerScripts->onFrame(frameDuration);
mProcessingInputEvents = false;
@ -274,14 +305,14 @@ namespace MWLua
void LuaManager::clear()
{
LuaUi::clearUserInterface();
LuaUi::clearGameInterface();
mUiResourceManager.clear();
MWBase::Environment::get().getWindowManager()->setConsoleMode("");
MWBase::Environment::get().getWorld()->getPostProcessor()->disableDynamicShaders();
mActiveLocalScripts.clear();
mLuaEvents.clear();
mEngineEvents.clear();
mInputEvents.clear();
mMenuInputEvents.clear();
mObjectLists.clear();
mGlobalScripts.removeAllScripts();
mGlobalScriptsStarted = false;
@ -293,7 +324,9 @@ namespace MWLua
mPlayer.getRefData().setLuaScripts(nullptr);
mPlayer = MWWorld::Ptr();
}
mGlobalStorage.setActive(true);
mGlobalStorage.clearTemporaryAndRemoveCallbacks();
mGlobalStorage.setActive(false);
mPlayerStorage.clearTemporaryAndRemoveCallbacks();
mInputActions.clear();
mInputTriggers.clear();
@ -314,7 +347,7 @@ namespace MWLua
if (!localScripts)
{
localScripts = createLocalScripts(ptr);
localScripts->addAutoStartedScripts();
mQueuedAutoStartedScripts.push_back(localScripts);
}
mActiveLocalScripts.insert(localScripts);
mEngineEvents.addToQueue(EngineEvents::OnActive{ getId(ptr) });
@ -322,17 +355,28 @@ namespace MWLua
void LuaManager::newGameStarted()
{
mGlobalStorage.setActive(true);
mInputEvents.clear();
mGlobalScripts.addAutoStartedScripts();
mGlobalScriptsStarted = true;
mNewGameStarted = true;
mMenuScripts.stateChanged();
}
void LuaManager::gameLoaded()
{
mGlobalStorage.setActive(true);
if (!mGlobalScriptsStarted)
mGlobalScripts.addAutoStartedScripts();
mGlobalScriptsStarted = true;
mMenuScripts.stateChanged();
}
void LuaManager::gameEnded()
{
// TODO: disable scripts and global storage when the game is actually unloaded
// mGlobalStorage.setActive(false);
mMenuScripts.stateChanged();
}
void LuaManager::uiModeChanged(const MWWorld::Ptr& arg)
@ -419,7 +463,7 @@ namespace MWLua
if (!autoStartConf.empty())
{
localScripts = createLocalScripts(ptr, std::move(autoStartConf));
localScripts->addAutoStartedScripts(); // TODO: put to a queue and apply on next `update()`
mQueuedAutoStartedScripts.push_back(localScripts);
}
}
if (localScripts)
@ -445,6 +489,7 @@ namespace MWLua
{
mInputEvents.push_back(event);
}
mMenuInputEvents.push_back(event);
}
MWBase::LuaManager::ActorControls* LuaManager::getActorControls(const MWWorld::Ptr& ptr) const
@ -526,6 +571,10 @@ namespace MWLua
throw std::runtime_error("Last generated RefNum is invalid");
MWBase::Environment::get().getWorldModel()->setLastGeneratedRefNum(lastGenerated);
// TODO: don't execute scripts right away, it will be necessary in multiplayer where global storage requires
// initialization. For now just set global storage as active slightly before it would be set by gameLoaded()
mGlobalStorage.setActive(true);
ESM::LuaScripts globalScripts;
globalScripts.load(reader);
mLuaEvents.load(mLua.sol(), reader, mContentFileMapping, mGlobalLoader.get());
@ -564,7 +613,9 @@ namespace MWLua
{
Log(Debug::Info) << "Reload Lua";
LuaUi::clearUserInterface();
LuaUi::clearGameInterface();
LuaUi::clearMenuInterface();
LuaUi::clearSettings();
MWBase::Environment::get().getWindowManager()->setConsoleMode("");
MWBase::Environment::get().getL10nManager()->dropCache();
mUiResourceManager.clear();
@ -573,26 +624,49 @@ namespace MWLua
mInputTriggers.clear();
initConfiguration();
{ // Reload global scripts
ESM::LuaScripts globalData;
if (mGlobalScriptsStarted)
{
mGlobalScripts.setSavedDataDeserializer(mGlobalSerializer.get());
ESM::LuaScripts data;
mGlobalScripts.save(data);
mGlobalScripts.save(globalData);
mGlobalStorage.clearTemporaryAndRemoveCallbacks();
mGlobalScripts.load(data);
}
std::unordered_map<ESM::RefNum, ESM::LuaScripts> localData;
for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView())
{ // Reload local scripts
{
LocalScripts* scripts = ptr.getRefData().getLuaScripts();
if (scripts == nullptr)
continue;
scripts->setSavedDataDeserializer(mLocalSerializer.get());
ESM::LuaScripts data;
scripts->save(data);
scripts->load(data);
localData[id] = data;
}
mMenuScripts.removeAllScripts();
mPlayerStorage.clearTemporaryAndRemoveCallbacks();
mMenuScripts.addAutoStartedScripts();
for (const auto& [id, ptr] : MWBase::Environment::get().getWorldModel()->getPtrRegistryView())
{
LocalScripts* scripts = ptr.getRefData().getLuaScripts();
if (scripts == nullptr)
continue;
scripts->load(localData[id]);
}
for (LocalScripts* scripts : mActiveLocalScripts)
scripts->setActive(true);
if (mGlobalScriptsStarted)
{
mGlobalScripts.load(globalData);
}
}
void LuaManager::handleConsoleCommand(
@ -601,16 +675,16 @@ namespace MWLua
PlayerScripts* playerScripts = nullptr;
if (!mPlayer.isEmpty())
playerScripts = dynamic_cast<PlayerScripts*>(mPlayer.getRefData().getLuaScripts());
if (!playerScripts)
bool processed = mMenuScripts.consoleCommand(consoleMode, command);
if (playerScripts)
{
MWBase::Environment::get().getWindowManager()->printToConsole(
"You must enter a game session to run Lua commands\n", MWBase::WindowManager::sConsoleColor_Error);
return;
sol::object selected = sol::nil;
if (!selectedPtr.isEmpty())
selected = sol::make_object(mLua.sol(), LObject(getId(selectedPtr)));
if (playerScripts->consoleCommand(consoleMode, command, selected))
processed = true;
}
sol::object selected = sol::nil;
if (!selectedPtr.isEmpty())
selected = sol::make_object(mLua.sol(), LObject(getId(selectedPtr)));
if (!playerScripts->consoleCommand(consoleMode, command, selected))
if (!processed)
MWBase::Environment::get().getWindowManager()->printToConsole(
"No Lua handlers for console\n", MWBase::WindowManager::sConsoleColor_Error);
}
@ -744,6 +818,7 @@ namespace MWLua
for (size_t i = 0; i < mConfiguration.size(); ++i)
{
bool isGlobal = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sGlobal;
bool isMenu = mConfiguration[i].mFlags & ESM::LuaScriptCfg::sMenu;
out << std::left;
out << " " << std::setw(nameW) << mConfiguration[i].mScriptPath;
@ -756,6 +831,8 @@ namespace MWLua
if (isGlobal)
out << std::setw(valueW * 2) << "NA (global script)";
else if (isMenu && (!selectedScripts || !selectedScripts->hasScript(i)))
out << std::setw(valueW * 2) << "NA (menu script)";
else if (selectedPtr.isEmpty())
out << std::setw(valueW * 2) << "NA (not selected) ";
else if (!selectedScripts || !selectedScripts->hasScript(i))

@ -18,6 +18,7 @@
#include "globalscripts.hpp"
#include "localscripts.hpp"
#include "luaevents.hpp"
#include "menuscripts.hpp"
#include "object.hpp"
#include "objectlists.hpp"
@ -34,6 +35,7 @@ namespace MWLua
LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir);
LuaManager(const LuaManager&) = delete;
LuaManager(LuaManager&&) = delete;
~LuaManager();
// Called by engine.cpp when the environment is fully initialized.
void init();
@ -67,6 +69,7 @@ namespace MWLua
// LuaManager queues these events and propagates to scripts on the next `update` call.
void newGameStarted() override;
void gameLoaded() override;
void gameEnded() override;
void objectAddedToScene(const MWWorld::Ptr& ptr) override;
void objectRemovedFromScene(const MWWorld::Ptr& ptr) override;
void inputEvent(const InputEvent& event) override;
@ -171,15 +174,18 @@ namespace MWLua
std::map<std::string, sol::object> mLocalPackages;
std::map<std::string, sol::object> mPlayerPackages;
MenuScripts mMenuScripts{ &mLua };
GlobalScripts mGlobalScripts{ &mLua };
std::set<LocalScripts*> mActiveLocalScripts;
std::vector<LocalScripts*> mQueuedAutoStartedScripts;
ObjectLists mObjectLists;
MWWorld::Ptr mPlayer;
LuaEvents mLuaEvents{ mGlobalScripts };
LuaEvents mLuaEvents{ mGlobalScripts, mMenuScripts };
EngineEvents mEngineEvents{ mGlobalScripts };
std::vector<MWBase::LuaManager::InputEvent> mInputEvents;
std::vector<MWBase::LuaManager::InputEvent> mMenuInputEvents;
std::unique_ptr<LuaUtil::UserdataSerializer> mGlobalSerializer;
std::unique_ptr<LuaUtil::UserdataSerializer> mLocalSerializer;

@ -0,0 +1,124 @@
#include "menuscripts.hpp"
#include <components/misc/strings/lower.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwstate/character.hpp"
namespace MWLua
{
static const MWState::Character* findCharacter(std::string_view characterDir)
{
MWBase::StateManager* manager = MWBase::Environment::get().getStateManager();
for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it)
if (it->getPath().filename() == characterDir)
return &*it;
return nullptr;
}
static const MWState::Slot* findSlot(const MWState::Character* character, std::string_view slotName)
{
if (!character)
return nullptr;
for (const MWState::Slot& slot : *character)
if (slot.mPath.filename() == slotName)
return &slot;
return nullptr;
}
sol::table initMenuPackage(const Context& context)
{
sol::state_view lua = context.mLua->sol();
sol::table api(lua, sol::create);
api["STATE"]
= LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs<std::string_view, MWBase::StateManager::State>({
{ "NoGame", MWBase::StateManager::State_NoGame },
{ "Running", MWBase::StateManager::State_Running },
{ "Ended", MWBase::StateManager::State_Ended },
}));
api["getState"] = []() -> int { return MWBase::Environment::get().getStateManager()->getState(); };
api["newGame"] = []() { MWBase::Environment::get().getStateManager()->requestNewGame(); };
api["loadGame"] = [](std::string_view dir, std::string_view slotName) {
const MWState::Character* character = findCharacter(dir);
const MWState::Slot* slot = findSlot(character, slotName);
if (!slot)
throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName));
MWBase::Environment::get().getStateManager()->requestLoad(slot->mPath);
};
api["deleteGame"] = [](std::string_view dir, std::string_view slotName) {
const MWState::Character* character = findCharacter(dir);
const MWState::Slot* slot = findSlot(character, slotName);
if (!slot)
throw std::runtime_error("Save game slot not found: " + std::string(dir) + "/" + std::string(slotName));
MWBase::Environment::get().getStateManager()->deleteGame(character, slot);
};
api["getCurrentSaveDir"] = []() -> sol::optional<std::string> {
MWBase::StateManager* manager = MWBase::Environment::get().getStateManager();
const MWState::Character* character = manager->getCurrentCharacter();
if (character)
return character->getPath().filename().string();
else
return sol::nullopt;
};
api["saveGame"] = [](std::string_view description, sol::optional<std::string_view> slotName) {
MWBase::StateManager* manager = MWBase::Environment::get().getStateManager();
const MWState::Character* character = manager->getCurrentCharacter();
const MWState::Slot* slot = nullptr;
if (slotName)
slot = findSlot(character, *slotName);
manager->saveGame(description, slot);
};
auto getSaves = [](sol::state_view lua, const MWState::Character& character) {
sol::table saves(lua, sol::create);
for (const MWState::Slot& slot : character)
{
sol::table slotInfo(lua, sol::create);
slotInfo["description"] = slot.mProfile.mDescription;
slotInfo["playerName"] = slot.mProfile.mPlayerName;
slotInfo["playerLevel"] = slot.mProfile.mPlayerLevel;
slotInfo["timePlayed"] = slot.mProfile.mTimePlayed;
sol::table contentFiles(lua, sol::create);
for (size_t i = 0; i < slot.mProfile.mContentFiles.size(); ++i)
contentFiles[i + 1] = Misc::StringUtils::lowerCase(slot.mProfile.mContentFiles[i]);
{
auto system_time = std::chrono::system_clock::now()
- (std::filesystem::file_time_type::clock::now() - slot.mTimeStamp);
slotInfo["creationTime"] = std::chrono::duration<double>(system_time.time_since_epoch()).count();
}
slotInfo["contentFiles"] = contentFiles;
saves[slot.mPath.filename().string()] = slotInfo;
}
return saves;
};
api["getSaves"] = [getSaves](sol::this_state lua, std::string_view dir) -> sol::table {
const MWState::Character* character = findCharacter(dir);
if (!character)
throw std::runtime_error("Saves not found: " + std::string(dir));
return getSaves(lua, *character);
};
api["getAllSaves"] = [getSaves](sol::this_state lua) -> sol::table {
sol::table saves(lua, sol::create);
MWBase::StateManager* manager = MWBase::Environment::get().getStateManager();
for (auto it = manager->characterBegin(); it != manager->characterEnd(); ++it)
saves[it->getPath().filename().string()] = getSaves(lua, *it);
return saves;
};
api["quit"] = []() { MWBase::Environment::get().getStateManager()->requestQuit(); };
return LuaUtil::makeReadOnly(api);
}
}

@ -0,0 +1,57 @@
#ifndef MWLUA_MENUSCRIPTS_H
#define MWLUA_MENUSCRIPTS_H
#include <SDL_events.h>
#include <components/lua/luastate.hpp>
#include <components/lua/scriptscontainer.hpp>
#include <components/sdlutil/events.hpp>
#include "../mwbase/luamanager.hpp"
#include "context.hpp"
#include "inputprocessor.hpp"
namespace MWLua
{
sol::table initMenuPackage(const Context& context);
class MenuScripts : public LuaUtil::ScriptsContainer
{
public:
MenuScripts(LuaUtil::LuaState* lua)
: LuaUtil::ScriptsContainer(lua, "Menu")
, mInputProcessor(this)
{
registerEngineHandlers({ &mOnFrameHandlers, &mStateChanged, &mConsoleCommandHandlers, &mUiModeChanged });
}
void processInputEvent(const MWBase::LuaManager::InputEvent& event)
{
mInputProcessor.processInputEvent(event);
}
void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); }
void stateChanged() { callEngineHandlers(mStateChanged); }
bool consoleCommand(const std::string& consoleMode, const std::string& command)
{
callEngineHandlers(mConsoleCommandHandlers, consoleMode, command);
return !mConsoleCommandHandlers.mList.empty();
}
void uiModeChanged() { callEngineHandlers(mUiModeChanged); }
private:
friend class MWLua::InputProcessor<MenuScripts>;
MWLua::InputProcessor<MenuScripts> mInputProcessor;
EngineHandlerList mOnFrameHandlers{ "onFrame" };
EngineHandlerList mStateChanged{ "onStateChanged" };
EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" };
EngineHandlerList mUiModeChanged{ "_onUiModeChanged" };
};
}
#endif // MWLUA_GLOBALSCRIPTS_H

@ -7,6 +7,7 @@
#include "../mwbase/luamanager.hpp"
#include "inputprocessor.hpp"
#include "localscripts.hpp"
namespace MWLua
@ -17,42 +18,14 @@ namespace MWLua
public:
PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj)
: LocalScripts(lua, obj)
, mInputProcessor(this)
{
registerEngineHandlers({ &mConsoleCommandHandlers, &mKeyPressHandlers, &mKeyReleaseHandlers,
&mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mOnFrameHandlers,
&mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved, &mQuestUpdate, &mUiModeChanged });
registerEngineHandlers({ &mConsoleCommandHandlers, &mOnFrameHandlers, &mQuestUpdate, &mUiModeChanged });
}
void processInputEvent(const MWBase::LuaManager::InputEvent& event)
{
using InputEvent = MWBase::LuaManager::InputEvent;
switch (event.mType)
{
case InputEvent::KeyPressed:
callEngineHandlers(mKeyPressHandlers, std::get<SDL_Keysym>(event.mValue));
break;
case InputEvent::KeyReleased:
callEngineHandlers(mKeyReleaseHandlers, std::get<SDL_Keysym>(event.mValue));
break;
case InputEvent::ControllerPressed:
callEngineHandlers(mControllerButtonPressHandlers, std::get<int>(event.mValue));
break;
case InputEvent::ControllerReleased:
callEngineHandlers(mControllerButtonReleaseHandlers, std::get<int>(event.mValue));
break;
case InputEvent::Action:
callEngineHandlers(mActionHandlers, std::get<int>(event.mValue));
break;
case InputEvent::TouchPressed:
callEngineHandlers(mTouchpadPressed, std::get<SDLUtil::TouchEvent>(event.mValue));
break;
case InputEvent::TouchReleased:
callEngineHandlers(mTouchpadReleased, std::get<SDLUtil::TouchEvent>(event.mValue));
break;
case InputEvent::TouchMoved:
callEngineHandlers(mTouchpadMoved, std::get<SDLUtil::TouchEvent>(event.mValue));
break;
}
mInputProcessor.processInputEvent(event);
}
void onFrame(float dt) { callEngineHandlers(mOnFrameHandlers, dt); }
@ -75,16 +48,10 @@ namespace MWLua
}
private:
friend class MWLua::InputProcessor<PlayerScripts>;
InputProcessor<PlayerScripts> mInputProcessor;
EngineHandlerList mConsoleCommandHandlers{ "onConsoleCommand" };
EngineHandlerList mKeyPressHandlers{ "onKeyPress" };
EngineHandlerList mKeyReleaseHandlers{ "onKeyRelease" };
EngineHandlerList mControllerButtonPressHandlers{ "onControllerButtonPress" };
EngineHandlerList mControllerButtonReleaseHandlers{ "onControllerButtonRelease" };
EngineHandlerList mActionHandlers{ "onInputAction" };
EngineHandlerList mOnFrameHandlers{ "onFrame" };
EngineHandlerList mTouchpadPressed{ "onTouchPress" };
EngineHandlerList mTouchpadReleased{ "onTouchRelease" };
EngineHandlerList mTouchpadMoved{ "onTouchMove" };
EngineHandlerList mQuestUpdate{ "onQuestUpdate" };
EngineHandlerList mUiModeChanged{ "_onUiModeChanged" };
};

@ -77,7 +77,11 @@ namespace MWLua
{
sol::table initAmbientPackage(const Context& context)
{
sol::table api(context.mLua->sol(), sol::create);
sol::state_view& lua = context.mLua->sol();
if (lua["openmw_ambient"] != sol::nil)
return lua["openmw_ambient"];
sol::table api(lua, sol::create);
api["playSound"] = [](std::string_view soundId, const sol::optional<sol::table>& options) {
auto args = getPlaySoundArgs(options);
@ -121,7 +125,8 @@ namespace MWLua
api["stopMusic"] = []() { MWBase::Environment::get().getSoundManager()->stopMusic(); };
return LuaUtil::makeReadOnly(api);
lua["openmw_ambient"] = LuaUtil::makeReadOnly(api);
return lua["openmw_ambient"];
}
sol::table initCoreSoundBindings(const Context& context)

@ -36,14 +36,18 @@ namespace sol
namespace MWLua
{
static void verifyPlayer(const Object& player)
{
if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr())
throw std::runtime_error("The argument must be a player!");
}
void addPlayerQuestBindings(sol::table& player, const Context& context)
void addPlayerBindings(sol::table player, const Context& context)
{
MWBase::Journal* const journal = MWBase::Environment::get().getJournal();
player["quests"] = [](const Object& player) {
if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr())
throw std::runtime_error("The argument must be a player!");
verifyPlayer(player);
bool allowChanges = dynamic_cast<const GObject*>(&player) != nullptr
|| dynamic_cast<const SelfObject*>(&player) != nullptr;
return Quests{ .mMutable = allowChanges };
@ -135,33 +139,30 @@ namespace MWLua
MWBase::InputManager* input = MWBase::Environment::get().getInputManager();
player["getControlSwitch"] = [input](const Object& player, std::string_view key) {
if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr())
throw std::runtime_error("The argument must be a player.");
verifyPlayer(player);
return input->getControlSwitch(key);
};
player["setControlSwitch"] = [input](const Object& player, std::string_view key, bool v) {
verifyPlayer(player);
if (dynamic_cast<const LObject*>(&player) && !dynamic_cast<const SelfObject*>(&player))
throw std::runtime_error("Only player and global scripts can toggle control switches.");
input->toggleControlSwitch(key, v);
};
player["isTeleportingEnabled"] = [](const Object& player) -> bool {
if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr())
throw std::runtime_error("The argument must be a player.");
verifyPlayer(player);
return MWBase::Environment::get().getWorld()->isTeleportingEnabled();
};
player["setTeleportingEnabled"] = [](const Object& player, bool state) {
if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr())
throw std::runtime_error("The argument must be a player.");
verifyPlayer(player);
if (dynamic_cast<const LObject*>(&player) && !dynamic_cast<const SelfObject*>(&player))
throw std::runtime_error("Only player and global scripts can toggle teleportation.");
MWBase::Environment::get().getWorld()->enableTeleporting(state);
};
player["setControlSwitch"] = [input](const Object& player, std::string_view key, bool v) {
if (player.ptr() != MWBase::Environment::get().getWorld()->getPlayerPtr())
throw std::runtime_error("The argument must be a player.");
if (dynamic_cast<const LObject*>(&player) && !dynamic_cast<const SelfObject*>(&player))
throw std::runtime_error("Only player and global scripts can toggle control switches.");
input->toggleControlSwitch(key, v);
player["sendMenuEvent"] = [context](const Object& player, std::string eventName, const sol::object& eventData) {
verifyPlayer(player);
context.mLuaEvents->addMenuEvent({ std::move(eventName), LuaUtil::serialize(eventData) });
};
}
void addPlayerBindings(sol::table player, const Context& context)
{
player["getCrimeLevel"] = [](const Object& o) -> int {
const MWWorld::Class& cls = o.ptr().getClass();
return cls.getNpcStats(o.ptr()).getBounty();
@ -169,6 +170,5 @@ namespace MWLua
player["isCharGenFinished"] = [](const Object&) -> bool {
return MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sCharGenState) == -1;
};
addPlayerQuestBindings(player, context);
}
}

@ -164,6 +164,10 @@ namespace MWLua
sol::table initTypesPackage(const Context& context)
{
auto* lua = context.mLua;
if (lua->sol()["openmw_types"] != sol::nil)
return lua->sol()["openmw_types"];
sol::table types(lua->sol(), sol::create);
auto addType = [&](std::string_view name, std::vector<ESM::RecNameInts> recTypes,
std::optional<std::string_view> base = std::nullopt) -> sol::table {
@ -255,6 +259,7 @@ namespace MWLua
packageToType[t] = type;
}
return LuaUtil::makeReadOnly(types);
lua->sol()["openmw_types"] = LuaUtil::makeReadOnly(types);
return lua->sol()["openmw_types"];
}
}

@ -89,33 +89,10 @@ namespace MWLua
}();
}
sol::table initUserInterfacePackage(const Context& context)
sol::table registerUiApi(const Context& context, bool menu)
{
MWBase::WindowManager* windowManager = MWBase::Environment::get().getWindowManager();
auto element = context.mLua->sol().new_usertype<LuaUi::Element>("Element");
element[sol::meta_function::to_string] = [](const LuaUi::Element& element) {
std::stringstream res;
res << "UiElement";
if (element.mLayer != "")
res << "[" << element.mLayer << "]";
return res.str();
};
element["layout"] = sol::property([](LuaUi::Element& element) { return element.mLayout; },
[](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; });
element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr<LuaUi::Element>& element) {
if (element->mDestroy || element->mUpdate)
return;
element->mUpdate = true;
luaManager->addAction([element] { wrapAction(element, [&] { element->update(); }); }, "Update UI");
};
element["destroy"] = [luaManager = context.mLuaManager](const std::shared_ptr<LuaUi::Element>& element) {
if (element->mDestroy)
return;
element->mDestroy = true;
luaManager->addAction([element] { wrapAction(element, [&] { element->destroy(); }); }, "Destroy UI");
};
sol::table api = context.mLua->newTable();
api["_setHudVisibility"] = [luaManager = context.mLuaManager](bool state) {
luaManager->addAction([state] { MWBase::Environment::get().getWindowManager()->setHudVisibility(state); });
@ -136,6 +113,7 @@ namespace MWLua
api["setConsoleMode"] = [luaManager = context.mLuaManager, windowManager](std::string_view mode) {
luaManager->addAction([mode = std::string(mode), windowManager] { windowManager->setConsoleMode(mode); });
};
api["getConsoleMode"] = [windowManager]() -> std::string_view { return windowManager->getConsoleMode(); };
api["setConsoleSelectedObject"] = [luaManager = context.mLuaManager, windowManager](const sol::object& obj) {
if (obj == sol::nil)
luaManager->addAction([windowManager] { windowManager->setConsoleSelectedObject(MWWorld::Ptr()); });
@ -148,24 +126,20 @@ namespace MWLua
}
};
api["content"] = LuaUi::loadContentConstructor(context.mLua);
api["create"] = [luaManager = context.mLuaManager](const sol::table& layout) {
auto element = LuaUi::Element::make(layout);
api["create"] = [luaManager = context.mLuaManager, menu](const sol::table& layout) {
auto element = LuaUi::Element::make(layout, menu);
luaManager->addAction([element] { wrapAction(element, [&] { element->create(); }); }, "Create UI");
return element;
};
api["updateAll"] = [context]() {
LuaUi::Element::forEach([](LuaUi::Element* e) { e->mUpdate = true; });
context.mLuaManager->addAction(
[]() { LuaUi::Element::forEach([](LuaUi::Element* e) { e->update(); }); }, "Update all UI elements");
api["updateAll"] = [luaManager = context.mLuaManager, menu]() {
LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { e->mUpdate = true; });
luaManager->addAction([menu]() { LuaUi::Element::forEach(menu, [](LuaUi::Element* e) { e->update(); }); },
"Update all menu UI elements");
};
api["_getMenuTransparency"] = []() -> float { return Settings::gui().mMenuTransparency; };
auto uiLayer = context.mLua->sol().new_usertype<LuaUi::Layer>("UiLayer");
uiLayer["name"] = sol::readonly_property([](LuaUi::Layer& self) -> std::string_view { return self.name(); });
uiLayer["size"] = sol::readonly_property([](LuaUi::Layer& self) { return self.size(); });
uiLayer[sol::meta_function::to_string]
= [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); };
sol::table layersTable = context.mLua->newTable();
layersTable["indexOf"] = [](std::string_view name) -> sol::optional<size_t> {
size_t index = LuaUi::Layer::indexOf(name);
@ -228,6 +202,7 @@ namespace MWLua
{ "Center", LuaUi::Alignment::Center }, { "End", LuaUi::Alignment::End } }));
api["registerSettingsPage"] = &LuaUi::registerSettingsPage;
api["removeSettingsPage"] = &LuaUi::removeSettingsPage;
api["texture"] = [luaManager = context.mLuaManager](const sol::table& options) {
LuaUi::TextureData data;
@ -305,6 +280,56 @@ namespace MWLua
// TODO
// api["_showMouseCursor"] = [](bool) {};
return LuaUtil::makeReadOnly(api);
return api;
}
sol::table initUserInterfacePackage(const Context& context)
{
std::string_view menuCache = "openmw_ui_menu";
std::string_view gameCache = "openmw_ui_game";
std::string_view cacheKey = context.mIsMenu ? menuCache : gameCache;
{
sol::state_view& lua = context.mLua->sol();
if (lua[cacheKey] != sol::nil)
return lua[cacheKey];
}
auto element = context.mLua->sol().new_usertype<LuaUi::Element>("UiElement");
element[sol::meta_function::to_string] = [](const LuaUi::Element& element) {
std::stringstream res;
res << "UiElement";
if (element.mLayer != "")
res << "[" << element.mLayer << "]";
return res.str();
};
element["layout"] = sol::property([](const LuaUi::Element& element) { return element.mLayout; },
[](LuaUi::Element& element, const sol::table& layout) { element.mLayout = layout; });
element["update"] = [luaManager = context.mLuaManager](const std::shared_ptr<LuaUi::Element>& element) {
if (element->mDestroy || element->mUpdate)
return;
element->mUpdate = true;
luaManager->addAction([element] { wrapAction(element, [&] { element->update(); }); }, "Update UI");
};
element["destroy"] = [luaManager = context.mLuaManager](const std::shared_ptr<LuaUi::Element>& element) {
if (element->mDestroy)
return;
element->mDestroy = true;
luaManager->addAction(
[element] { wrapAction(element, [&] { LuaUi::Element::erase(element.get()); }); }, "Destroy UI");
};
auto uiLayer = context.mLua->sol().new_usertype<LuaUi::Layer>("UiLayer");
uiLayer["name"] = sol::readonly_property([](LuaUi::Layer& self) { return self.name(); });
uiLayer["size"] = sol::readonly_property([](LuaUi::Layer& self) { return self.size(); });
uiLayer[sol::meta_function::to_string]
= [](LuaUi::Layer& self) { return Misc::StringUtils::format("UiLayer(%s)", self.name()); };
sol::table menuApi = registerUiApi(context, true);
sol::table gameApi = registerUiApi(context, false);
sol::state_view& lua = context.mLua->sol();
lua[menuCache] = LuaUtil::makeReadOnly(menuApi);
lua[gameCache] = LuaUtil::makeReadOnly(gameApi);
return lua[cacheKey];
}
}

@ -0,0 +1,215 @@
#include "worldbindings.hpp"
#include <components/esm3/loadacti.hpp>
#include <components/esm3/loadalch.hpp>
#include <components/esm3/loadarmo.hpp>
#include <components/esm3/loadbook.hpp>
#include <components/esm3/loadclot.hpp>
#include <components/esm3/loadmisc.hpp>
#include <components/esm3/loadskil.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/lua/luastate.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/action.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/datetimemanager.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/manualref.hpp"
#include "../mwworld/store.hpp"
#include "../mwworld/worldmodel.hpp"
#include "luamanagerimp.hpp"
#include "corebindings.hpp"
#include "mwscriptbindings.hpp"
namespace MWLua
{
struct CellsStore
{
};
}
namespace sol
{
template <>
struct is_automagical<MWLua::CellsStore> : std::false_type
{
};
}
namespace MWLua
{
static void checkGameInitialized(LuaUtil::LuaState* lua)
{
if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame)
throw std::runtime_error(
"This function cannot be used until the game is fully initialized.\n" + lua->debugTraceback());
}
static void addWorldTimeBindings(sol::table& api, const Context& context)
{
MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager();
api["setGameTimeScale"] = [timeManager](double scale) { timeManager->setGameTimeScale(scale); };
api["setSimulationTimeScale"] = [context, timeManager](float scale) {
context.mLuaManager->addAction([scale, timeManager] { timeManager->setSimulationTimeScale(scale); });
};
api["pause"]
= [timeManager](sol::optional<std::string_view> tag) { timeManager->pause(tag.value_or("paused")); };
api["unpause"]
= [timeManager](sol::optional<std::string_view> tag) { timeManager->unpause(tag.value_or("paused")); };
api["getPausedTags"] = [timeManager](sol::this_state lua) {
sol::table res(lua, sol::create);
for (const std::string& tag : timeManager->getPausedTags())
res[tag] = tag;
return res;
};
}
static void addCellGetters(sol::table& api, const Context& context)
{
api["getCellByName"] = [](std::string_view name) {
return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(name, /*forceLoad=*/false) };
};
api["getExteriorCell"] = [](int x, int y, sol::object cellOrName) {
ESM::RefId worldspace;
if (cellOrName.is<GCell>())
worldspace = cellOrName.as<GCell>().mStore->getCell()->getWorldSpace();
else if (cellOrName.is<std::string_view>() && !cellOrName.as<std::string_view>().empty())
worldspace = MWBase::Environment::get()
.getWorldModel()
->getCell(cellOrName.as<std::string_view>())
.getCell()
->getWorldSpace();
else
worldspace = ESM::Cell::sDefaultWorldspaceId;
return GCell{ &MWBase::Environment::get().getWorldModel()->getExterior(
ESM::ExteriorCellLocation(x, y, worldspace), /*forceLoad=*/false) };
};
const MWWorld::Store<ESM::Cell>* cells3Store = &MWBase::Environment::get().getESMStore()->get<ESM::Cell>();
const MWWorld::Store<ESM4::Cell>* cells4Store = &MWBase::Environment::get().getESMStore()->get<ESM4::Cell>();
sol::usertype<CellsStore> cells = context.mLua->sol().new_usertype<CellsStore>("Cells");
cells[sol::meta_function::length]
= [cells3Store, cells4Store](const CellsStore&) { return cells3Store->getSize() + cells4Store->getSize(); };
cells[sol::meta_function::index]
= [cells3Store, cells4Store](const CellsStore&, size_t index) -> sol::optional<GCell> {
if (index > cells3Store->getSize() + cells3Store->getSize() || index == 0)
return sol::nullopt;
index--; // Translate from Lua's 1-based indexing.
if (index < cells3Store->getSize())
{
const ESM::Cell* cellRecord = cells3Store->at(index);
return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(
cellRecord->mId, /*forceLoad=*/false) };
}
else
{
const ESM4::Cell* cellRecord = cells4Store->at(index - cells3Store->getSize());
return GCell{ &MWBase::Environment::get().getWorldModel()->getCell(
cellRecord->mId, /*forceLoad=*/false) };
}
};
cells[sol::meta_function::pairs] = context.mLua->sol()["ipairsForArray"].template get<sol::function>();
cells[sol::meta_function::ipairs] = context.mLua->sol()["ipairsForArray"].template get<sol::function>();
api["cells"] = CellsStore{};
}
sol::table initWorldPackage(const Context& context)
{
sol::table api(context.mLua->sol(), sol::create);
addCoreTimeBindings(api, context);
addWorldTimeBindings(api, context);
addCellGetters(api, context);
api["mwscript"] = initMWScriptBindings(context);
ObjectLists* objectLists = context.mObjectLists;
api["activeActors"] = GObjectList{ objectLists->getActorsInScene() };
api["players"] = GObjectList{ objectLists->getPlayers() };
api["createObject"] = [lua = context.mLua](std::string_view recordId, sol::optional<int> count) -> GObject {
checkGameInitialized(lua);
MWWorld::ManualRef mref(*MWBase::Environment::get().getESMStore(), ESM::RefId::deserializeText(recordId));
const MWWorld::Ptr& ptr = mref.getPtr();
ptr.getRefData().disable();
MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getDraftCell();
MWWorld::Ptr newPtr = ptr.getClass().copyToCell(ptr, cell, count.value_or(1));
return GObject(newPtr);
};
api["getObjectByFormId"] = [](std::string_view formIdStr) -> GObject {
ESM::RefId refId = ESM::RefId::deserializeText(formIdStr);
if (!refId.is<ESM::FormId>())
throw std::runtime_error("FormId expected, got " + std::string(formIdStr) + "; use core.getFormId");
return GObject(*refId.getIf<ESM::FormId>());
};
// Creates a new record in the world database.
api["createRecord"] = sol::overload(
[lua = context.mLua](const ESM::Activator& activator) -> const ESM::Activator* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(activator);
},
[lua = context.mLua](const ESM::Armor& armor) -> const ESM::Armor* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(armor);
},
[lua = context.mLua](const ESM::Clothing& clothing) -> const ESM::Clothing* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(clothing);
},
[lua = context.mLua](const ESM::Book& book) -> const ESM::Book* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(book);
},
[lua = context.mLua](const ESM::Miscellaneous& misc) -> const ESM::Miscellaneous* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(misc);
},
[lua = context.mLua](const ESM::Potion& potion) -> const ESM::Potion* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(potion);
},
[lua = context.mLua](const ESM::Weapon& weapon) -> const ESM::Weapon* {
checkGameInitialized(lua);
return MWBase::Environment::get().getESMStore()->insert(weapon);
});
api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) {
if (!object.ptr().getRefData().activate())
return;
context.mLuaManager->addAction(
[object, actor] {
const MWWorld::Ptr& objPtr = object.ptr();
const MWWorld::Ptr& actorPtr = actor.ptr();
objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr);
},
"_runStandardActivationAction");
};
api["_runStandardUseAction"] = [context](const GObject& object, const GObject& actor, bool force) {
context.mLuaManager->addAction(
[object, actor, force] {
const MWWorld::Ptr& actorPtr = actor.ptr();
const MWWorld::Ptr& objectPtr = object.ptr();
if (actorPtr == MWBase::Environment::get().getWorld()->getPlayerPtr())
MWBase::Environment::get().getWindowManager()->useItem(objectPtr, force);
else
{
std::unique_ptr<MWWorld::Action> action = objectPtr.getClass().use(objectPtr, force);
action->execute(actorPtr, true);
}
},
"_runStandardUseAction");
};
return LuaUtil::makeReadOnly(api);
}
}

@ -0,0 +1,13 @@
#ifndef MWLUA_WORLDBINDINGS_H
#define MWLUA_WORLDBINDINGS_H
#include <sol/forward.hpp>
#include "context.hpp"
namespace MWLua
{
sol::table initWorldPackage(const Context&);
}
#endif // MWLUA_WORLDBINDINGS_H

@ -68,6 +68,8 @@ void MWState::StateManager::cleanup(bool force)
mLastSavegame.clear();
MWMechanics::CreatureStats::cleanup();
endGame();
}
MWBase::Environment::get().getLuaManager()->clear();
}
@ -170,10 +172,10 @@ void MWState::StateManager::newGame(bool bypass)
{
Log(Debug::Info) << "Starting a new game";
MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup();
MWBase::Environment::get().getLuaManager()->newGameStarted();
MWBase::Environment::get().getWorld()->startNewGame(bypass);
mState = State_Running;
MWBase::Environment::get().getLuaManager()->newGameStarted();
MWBase::Environment::get().getWindowManager()->fadeScreenOut(0);
MWBase::Environment::get().getWindowManager()->fadeScreenIn(1);
@ -197,11 +199,13 @@ void MWState::StateManager::newGame(bool bypass)
void MWState::StateManager::endGame()
{
mState = State_Ended;
MWBase::Environment::get().getLuaManager()->gameEnded();
}
void MWState::StateManager::resumeGame()
{
mState = State_Running;
MWBase::Environment::get().getLuaManager()->gameLoaded();
}
void MWState::StateManager::saveGame(std::string_view description, const Slot* slot)
@ -744,6 +748,18 @@ void MWState::StateManager::update(float duration)
MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu);
}
}
if (mNewGameRequest)
{
newGame();
mNewGameRequest = false;
}
if (mLoadRequest)
{
loadGame(*mLoadRequest);
mLoadRequest = std::nullopt;
}
}
bool MWState::StateManager::confirmLoading(const std::vector<std::string_view>& missingFiles) const

@ -14,6 +14,8 @@ namespace MWState
{
bool mQuitRequest;
bool mAskLoadRecent;
bool mNewGameRequest = false;
std::optional<std::filesystem::path> mLoadRequest;
State mState;
CharacterManager mCharacterManager;
double mTimePlayed;
@ -39,6 +41,9 @@ namespace MWState
void askLoadRecent() override;
void requestNewGame() override { mNewGameRequest = true; }
void requestLoad(const std::filesystem::path& filepath) override { mLoadRequest = filepath; }
State getState() const override;
void newGame(bool bypass = false) override;

@ -4,6 +4,7 @@
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
@ -263,8 +264,9 @@ namespace MWWorld
void DateTimeManager::updateIsPaused()
{
auto stateManager = MWBase::Environment::get().getStateManager();
auto wm = MWBase::Environment::get().getWindowManager();
mPaused = !mPausedTags.empty() || wm->isConsoleMode() || wm->isPostProcessorHudVisible()
|| wm->isInteractiveMessageBoxActive();
|| wm->isInteractiveMessageBoxActive() || stateManager->getState() == MWBase::StateManager::State_NoGame;
}
}

@ -22,6 +22,7 @@ namespace
sol::state_view& mLua = luaState.sol();
LuaUtil::LuaStorage::initLuaBindings(mLua);
LuaUtil::LuaStorage storage(mLua);
storage.setActive(true);
std::vector<std::string> callbackCalls;
sol::table callbackHiddenData(mLua, sol::create);
@ -65,6 +66,7 @@ namespace
sol::state mLua;
LuaUtil::LuaStorage::initLuaBindings(mLua);
LuaUtil::LuaStorage storage(mLua);
storage.setActive(true);
mLua["mutable"] = storage.getMutableSection("test");
mLua["ro"] = storage.getReadOnlySection("test");
@ -82,6 +84,7 @@ namespace
sol::state mLua;
LuaUtil::LuaStorage::initLuaBindings(mLua);
LuaUtil::LuaStorage storage(mLua);
storage.setActive(true);
mLua["permanent"] = storage.getMutableSection("permanent");
mLua["temporary"] = storage.getMutableSection("temporary");
@ -104,6 +107,7 @@ namespace
mLua.safe_script("permanent:set('z', 4)");
LuaUtil::LuaStorage storage2(mLua);
storage2.setActive(true);
storage2.load(tmpFile);
mLua["permanent"] = storage2.getMutableSection("permanent");
mLua["temporary"] = storage2.getMutableSection("temporary");

@ -20,8 +20,10 @@ namespace ESM
static constexpr Flags sCustom = 1ull << 1; // local; can be attached/detached by a global script
static constexpr Flags sPlayer = 1ull << 2; // auto attach to players
static constexpr Flags sMerge = 1ull
<< 3; // merge with configuration for this script from previous content files.
// merge with configuration for this script from previous content files.
static constexpr Flags sMerge = 1ull << 3;
static constexpr Flags sMenu = 1ull << 4; // start as a menu script
std::string mScriptPath; // VFS path to the script.
std::string mInitializationData; // Serialized Lua table. It is a binary data. Can contain '\0'.

@ -18,6 +18,7 @@ namespace LuaUtil
{ "GLOBAL", ESM::LuaScriptCfg::sGlobal },
{ "CUSTOM", ESM::LuaScriptCfg::sCustom },
{ "PLAYER", ESM::LuaScriptCfg::sPlayer },
{ "MENU", ESM::LuaScriptCfg::sMenu },
};
const std::map<std::string, ESM::RecNameInts, std::less<>> typeTagsByName{

@ -22,6 +22,7 @@ namespace LuaUtil
std::optional<int> findId(std::string_view path) const;
bool isCustomScript(int id) const { return mScripts[id].mFlags & ESM::LuaScriptCfg::sCustom; }
ScriptIdsWithInitializationData getMenuConf() const { return getConfByFlag(ESM::LuaScriptCfg::sMenu); }
ScriptIdsWithInitializationData getGlobalConf() const { return getConfByFlag(ESM::LuaScriptCfg::sGlobal); }
ScriptIdsWithInitializationData getPlayerConf() const { return getConfByFlag(ESM::LuaScriptCfg::sPlayer); }
ScriptIdsWithInitializationData getLocalConf(

@ -31,6 +31,7 @@ namespace LuaUtil
const LuaStorage::Value& LuaStorage::Section::get(std::string_view key) const
{
checkIfActive();
auto it = mValues.find(key);
if (it != mValues.end())
return it->second;
@ -49,6 +50,14 @@ namespace LuaUtil
return !valid;
}),
mCallbacks.end());
mMenuScriptsCallbacks.erase(std::remove_if(mMenuScriptsCallbacks.begin(), mMenuScriptsCallbacks.end(),
[&](const Callback& callback) {
bool valid = callback.isValid();
if (valid)
callback.tryCall(mSectionName, changedKey);
return !valid;
}),
mMenuScriptsCallbacks.end());
mStorage->mRunningCallbacks.erase(this);
}
@ -64,6 +73,7 @@ namespace LuaUtil
void LuaStorage::Section::set(std::string_view key, const sol::object& value)
{
checkIfActive();
throwIfCallbackRecursionIsTooDeep();
if (value != sol::nil)
mValues[std::string(key)] = Value(value);
@ -80,6 +90,7 @@ namespace LuaUtil
void LuaStorage::Section::setAll(const sol::optional<sol::table>& values)
{
checkIfActive();
throwIfCallbackRecursionIsTooDeep();
mValues.clear();
if (values)
@ -94,6 +105,7 @@ namespace LuaUtil
sol::table LuaStorage::Section::asTable()
{
checkIfActive();
sol::table res(mStorage->mLua, sol::create);
for (const auto& [k, v] : mValues)
res[k] = v.getCopy(mStorage->mLua);
@ -112,7 +124,8 @@ namespace LuaUtil
};
sview["asTable"] = [](const SectionView& section) { return section.mSection->asTable(); };
sview["subscribe"] = [](const SectionView& section, const sol::table& callback) {
std::vector<Callback>& callbacks = section.mSection->mCallbacks;
std::vector<Callback>& callbacks
= section.mForMenuScripts ? section.mSection->mMenuScriptsCallbacks : section.mSection->mCallbacks;
if (!callbacks.empty() && callbacks.size() == callbacks.capacity())
{
callbacks.erase(
@ -166,14 +179,29 @@ namespace LuaUtil
return LuaUtil::makeReadOnly(res);
}
sol::table LuaStorage::initMenuPackage(lua_State* lua, LuaStorage* globalStorage, LuaStorage* playerStorage)
{
sol::table res(lua, sol::create);
res["playerSection"] = [playerStorage](std::string_view section) {
return playerStorage->getMutableSection(section, /*forMenuScripts=*/true);
};
res["globalSection"]
= [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); };
res["allPlayerSections"] = [playerStorage]() { return playerStorage->getAllSections(); };
return LuaUtil::makeReadOnly(res);
}
void LuaStorage::clearTemporaryAndRemoveCallbacks()
{
auto it = mData.begin();
while (it != mData.end())
{
it->second->mCallbacks.clear();
// Note that we don't clear menu callbacks for permanent sections
// because starting/loading a game doesn't reset menu scripts.
if (!it->second->mPermanent)
{
it->second->mMenuScriptsCallbacks.clear();
it->second->mValues.clear();
it = mData.erase(it);
}
@ -222,6 +250,7 @@ namespace LuaUtil
const std::shared_ptr<LuaStorage::Section>& LuaStorage::getSection(std::string_view sectionName)
{
checkIfActive();
auto it = mData.find(sectionName);
if (it != mData.end())
return it->second;
@ -231,14 +260,16 @@ namespace LuaUtil
return newIt->second;
}
sol::object LuaStorage::getSection(std::string_view sectionName, bool readOnly)
sol::object LuaStorage::getSection(std::string_view sectionName, bool readOnly, bool forMenuScripts)
{
checkIfActive();
const std::shared_ptr<Section>& section = getSection(sectionName);
return sol::make_object<SectionView>(mLua, SectionView{ section, readOnly });
return sol::make_object<SectionView>(mLua, SectionView{ section, readOnly, forMenuScripts });
}
sol::table LuaStorage::getAllSections(bool readOnly)
{
checkIfActive();
sol::table res(mLua, sol::create);
for (const auto& [sectionName, _] : mData)
res[sectionName] = getSection(sectionName, readOnly);

@ -3,6 +3,7 @@
#include <map>
#include <sol/sol.hpp>
#include <stdexcept>
#include "asyncpackage.hpp"
#include "serialization.hpp"
@ -17,9 +18,11 @@ namespace LuaUtil
static sol::table initGlobalPackage(lua_State* lua, LuaStorage* globalStorage);
static sol::table initLocalPackage(lua_State* lua, LuaStorage* globalStorage);
static sol::table initPlayerPackage(lua_State* lua, LuaStorage* globalStorage, LuaStorage* playerStorage);
static sol::table initMenuPackage(lua_State* lua, LuaStorage* globalStorage, LuaStorage* playerStorage);
explicit LuaStorage(lua_State* lua)
: mLua(lua)
, mActive(false)
{
}
@ -27,8 +30,11 @@ namespace LuaUtil
void load(const std::filesystem::path& path);
void save(const std::filesystem::path& path) const;
sol::object getSection(std::string_view sectionName, bool readOnly);
sol::object getMutableSection(std::string_view sectionName) { return getSection(sectionName, false); }
sol::object getSection(std::string_view sectionName, bool readOnly, bool forMenuScripts = false);
sol::object getMutableSection(std::string_view sectionName, bool forMenuScripts = false)
{
return getSection(sectionName, false, forMenuScripts);
}
sol::object getReadOnlySection(std::string_view sectionName) { return getSection(sectionName, true); }
sol::table getAllSections(bool readOnly = false);
@ -51,6 +57,7 @@ namespace LuaUtil
virtual void sectionReplaced(std::string_view section, const sol::optional<sol::table>& values) const = 0;
};
void setListener(const Listener* listener) { mListener = listener; }
void setActive(bool active) { mActive = active; }
private:
class Value
@ -87,13 +94,18 @@ namespace LuaUtil
std::string mSectionName;
std::map<std::string, Value, std::less<>> mValues;
std::vector<Callback> mCallbacks;
std::vector<Callback> mMenuScriptsCallbacks; // menu callbacks are in a separate vector because we don't
// remove them in clear()
bool mPermanent = true;
static Value sEmpty;
void checkIfActive() const { mStorage->checkIfActive(); }
};
struct SectionView
{
std::shared_ptr<Section> mSection;
bool mReadOnly;
bool mForMenuScripts = false;
};
const std::shared_ptr<Section>& getSection(std::string_view sectionName);
@ -102,6 +114,12 @@ namespace LuaUtil
std::map<std::string_view, std::shared_ptr<Section>> mData;
const Listener* mListener = nullptr;
std::set<const Section*> mRunningCallbacks;
bool mActive;
void checkIfActive() const
{
if (!mActive)
throw std::logic_error("Trying to access inactive storage");
}
};
}

@ -240,7 +240,8 @@ namespace LuaUi
}
}
std::map<Element*, std::shared_ptr<Element>> Element::sAllElements;
std::map<Element*, std::shared_ptr<Element>> Element::sMenuElements;
std::map<Element*, std::shared_ptr<Element>> Element::sGameElements;
Element::Element(sol::table layout)
: mRoot(nullptr)
@ -251,13 +252,21 @@ namespace LuaUi
{
}
std::shared_ptr<Element> Element::make(sol::table layout)
std::shared_ptr<Element> Element::make(sol::table layout, bool menu)
{
std::shared_ptr<Element> ptr(new Element(std::move(layout)));
sAllElements[ptr.get()] = ptr;
auto& container = menu ? sMenuElements : sGameElements;
container[ptr.get()] = ptr;
return ptr;
}
void Element::erase(Element* element)
{
element->destroy();
sMenuElements.erase(element);
sGameElements.erase(element);
}
void Element::create()
{
assert(!mRoot);
@ -303,6 +312,5 @@ namespace LuaUi
mRoot = nullptr;
mLayout = sol::make_object(mLayout.lua_state(), sol::nil);
}
sAllElements.erase(this);
}
}

@ -7,13 +7,15 @@ namespace LuaUi
{
struct Element
{
static std::shared_ptr<Element> make(sol::table layout);
static std::shared_ptr<Element> make(sol::table layout, bool menu);
static void erase(Element* element);
template <class Callback>
static void forEach(Callback callback)
static void forEach(bool menu, Callback callback)
{
for (auto& [e, _] : sAllElements)
callback(e);
auto& container = menu ? sMenuElements : sGameElements;
for (auto& [_, element] : container)
callback(element.get());
}
WidgetExtension* mRoot;
@ -28,12 +30,14 @@ namespace LuaUi
void destroy();
friend void clearUserInterface();
friend void clearGameInterface();
friend void clearMenuInterface();
private:
Element(sol::table layout);
sol::table layout() { return LuaUtil::cast<sol::table>(mLayout); }
static std::map<Element*, std::shared_ptr<Element>> sAllElements;
static std::map<Element*, std::shared_ptr<Element>> sGameElements;
static std::map<Element*, std::shared_ptr<Element>> sMenuElements;
};
}

@ -8,6 +8,7 @@ namespace LuaUi
// implemented in scriptsettings.cpp
void registerSettingsPage(const sol::table& options);
void clearSettings();
void removeSettingsPage(const sol::table& options);
}
#endif // !OPENMW_LUAUI_REGISTERSCRIPTSETTINGS

@ -40,6 +40,11 @@ namespace LuaUi
allPages.push_back(options);
}
void removeSettingsPage(const sol::table& options)
{
std::erase_if(allPages, [options](const sol::table& it) { return it == options; });
}
void clearSettings()
{
allPages.clear();
@ -47,10 +52,10 @@ namespace LuaUi
void attachPageAt(size_t index, LuaAdapter* adapter)
{
adapter->detach();
if (index < allPages.size())
{
ScriptSettingsPage page = parse(allPages[index]);
adapter->detach();
if (page.mElement.get())
adapter->attach(page.mElement);
}

@ -3,7 +3,6 @@
#include <memory>
#include <string>
#include <string_view>
namespace LuaUi
{

@ -11,6 +11,9 @@ namespace LuaUi
{
MYGUI_RTTI_DERIVED(LuaTextEdit)
public:
bool isTextInput() override { return mEditBox->getEditStatic(); }
protected:
void initialize() override;
void deinitialize() override;

@ -44,10 +44,15 @@ namespace LuaUi
return types;
}
void clearUserInterface()
void clearGameInterface()
{
clearSettings();
while (!Element::sAllElements.empty())
Element::sAllElements.begin()->second->destroy();
while (!Element::sGameElements.empty())
Element::erase(Element::sGameElements.begin()->second.get());
}
void clearMenuInterface()
{
while (!Element::sMenuElements.empty())
Element::erase(Element::sMenuElements.begin()->second.get());
}
}

@ -10,7 +10,8 @@ namespace LuaUi
const std::unordered_map<std::string, std::string>& widgetTypeToName();
void clearUserInterface();
void clearGameInterface();
void clearMenuInterface();
}
#endif // OPENMW_LUAUI_WIDGETLIST

@ -73,6 +73,8 @@ namespace LuaUi
virtual MyGUI::IntPoint calculatePosition(const MyGUI::IntSize& size);
MyGUI::IntCoord calculateCoord();
virtual bool isTextInput() { return false; }
protected:
virtual void initialize();
void registerEvents(MyGUI::Widget* w);

@ -2,8 +2,8 @@ paths=(
openmw_aux/*lua
scripts/omw/activationhandlers.lua
scripts/omw/ai.lua
scripts/omw/input/playercontrols.lua
scripts/omw/mechanics/animationcontroller.lua
scripts/omw/playercontrols.lua
scripts/omw/camera/camera.lua
scripts/omw/mwui/init.lua
scripts/omw/settings/player.lua

@ -28,6 +28,7 @@ Lua API reference
openmw_camera
openmw_postprocessing
openmw_debug
openmw_menu
openmw_aux_calendar
openmw_aux_util
openmw_aux_time

@ -5,9 +5,16 @@ Engine handlers reference
Engine handler is a function defined by a script, that can be called by the engine.
**Can be defined by any script**
.. list-table::
:widths: 20 80
**Can be defined by any script**
* - onInterfaceOverride(base)
- | Called if the current script has an interface and overrides an interface
| (``base``) of another script.
**Can be defined by any non-menu script**
.. list-table::
:widths: 20 80
@ -29,9 +36,6 @@ Engine handler is a function defined by a script, that can be called by the engi
| Note that ``onLoad`` means loading a script rather than loading a game.
| If a script did not exist when a game was saved onLoad will not be
| called, but ``onInit`` will.
* - onInterfaceOverride(base)
- | Called if the current script has an interface and overrides an interface
| (``base``) of another script.
**Only for global scripts**
@ -80,7 +84,7 @@ Engine handler is a function defined by a script, that can be called by the engi
| Similarly to onActivated, the item has already been removed
| from the actor's inventory, and the count was set to zero.
**Only for local scripts attached to a player**
**Only menu scripts and local scripts attached to a player**
.. list-table::
:widths: 20 80
@ -94,8 +98,6 @@ Engine handler is a function defined by a script, that can be called by the engi
- | `Key <openmw_input.html##(KeyboardEvent)>`_ is pressed.
| Usage example:
| ``if key.symbol == 'z' and key.withShift then ...``
* - onQuestUpdate(questId, stage)
- | Called when a quest is updated.
* - onKeyRelease(key)
- | `Key <openmw_input.html##(KeyboardEvent)>`_ is released.
| Usage example:
@ -127,3 +129,23 @@ Engine handler is a function defined by a script, that can be called by the engi
- | User entered `command` in in-game console. Called if either
| `mode` is not default or `command` starts with prefix `lua`.
**Only for local scripts attached to a player**
.. list-table::
:widths: 20 80
* - onKeyPress(key)
- | `Key <openmw_input.html##(KeyboardEvent)>`_ is pressed.
| Usage example:
| ``if key.symbol == 'z' and key.withShift then ...``
* - onQuestUpdate(questId, stage)
- | Called when a quest is updated.
**Only for menu scripts**
.. list-table::
:widths: 20 80
* - onStateChanged()
- | Called whenever the current game changes
| (i. e. the result of `getState <openmw_menu.html##(getState)>`_ changes)

@ -0,0 +1,7 @@
Package openmw.menu
======================
.. include:: version.rst
.. raw:: html
:file: generated_html/openmw_menu.html

@ -70,6 +70,9 @@ Cell
Global scripts
Lua scripts that are not attached to any game object and are always active. Global scripts can not be started or stopped during a game session. Lists of global scripts are defined by `omwscripts` files, which should be :ref:`registered <Lua scripting>` in `openmw.cfg`.
Menu scripts
Lua scripts that are ran regardless of a game being loaded. They can be used to add features to the main menu and manage save files.
Local scripts
Lua scripts that are attached to some game object. A local script is active only if the object it is attached to is in an active cell. There are no limitations to the number of local scripts on one object. Local scripts can be attached to (or detached from) any object at any moment by a global script. In some cases inactive local scripts still can run code (for example during saving and loading), but while inactive they can not see nearby objects.
@ -173,6 +176,7 @@ The order of lines determines the script load order (i.e. script priorities).
Possible flags are:
- ``GLOBAL`` - a global script; always active, can not be stopped;
- ``MENU`` - a menu script; always active, even before a game is loaded
- ``CUSTOM`` - dynamic local script that can be started or stopped by a global script;
- ``PLAYER`` - an auto started player script;
- ``ACTIVATOR`` - a local script that will be automatically attached to any activator;
@ -474,6 +478,12 @@ This is another kind of script-to-script interactions. The differences:
- Event handlers can not return any data to the sender.
- Event handlers have a single argument `eventData` (must be :ref:`serializable <Serializable data>`)
There are a few methods for sending events:
- `core.sendGlobalEvent <openmw_core.html##(sendGlobalEvent)>`_ to send events to global scripts
- `GameObject:sendEvent <openmw_core.html##(GameObject).sendEvent>`_ to send events to local scripts attached to a game object
- `types.Player.sendMenuEvent <openmw_menu.html##(Player).sendMenuEvent>`_ to send events to menu scripts of the given player
Events are the main way of interacting between local and global scripts.
They are not recommended for interactions between two global scripts, because in this case interfaces are more convenient.
@ -614,7 +624,7 @@ Also in `openmw_aux`_ is the helper function ``runRepeatedly``, it is implemente
local core = require('openmw.core')
local time = require('openmw_aux.time')
-- call `doSomething()` at the end of every game day.
-- call `doSomething()` at the end of every game day.
-- the second argument (`time.day`) is the interval.
-- the periodical evaluation can be stopped at any moment by calling `stopFn()`
local timeBeforeMidnight = time.day - core.getGameTime() % time.day

@ -31,6 +31,8 @@
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls :ref:`user interface <User interface reference>`. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.menu <Package openmw.menu>` | by menu scripts | | Main menu functionality, such as managing game saves |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.camera <Package openmw.camera>` | by player scripts | | Controls camera. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.postprocessing <Package openmw.postprocessing>`| by player scripts | | Controls post-process shaders. |

@ -72,16 +72,16 @@ set(BUILTIN_DATA_FILES
scripts/omw/camera/settings.lua
scripts/omw/camera/move360.lua
scripts/omw/camera/first_person_auto_switch.lua
scripts/omw/console/player.lua
scripts/omw/console/global.lua
scripts/omw/console/local.lua
scripts/omw/console/player.lua
scripts/omw/console/menu.lua
scripts/omw/mechanics/animationcontroller.lua
scripts/omw/mechanics/playercontroller.lua
scripts/omw/playercontrols.lua
scripts/omw/settings/menu.lua
scripts/omw/settings/player.lua
scripts/omw/settings/global.lua
scripts/omw/settings/common.lua
scripts/omw/settings/render.lua
scripts/omw/settings/renderers.lua
scripts/omw/mwui/constants.lua
scripts/omw/mwui/borders.lua
@ -93,6 +93,8 @@ set(BUILTIN_DATA_FILES
scripts/omw/ui.lua
scripts/omw/usehandlers.lua
scripts/omw/worldeventhandlers.lua
scripts/omw/input/settings.lua
scripts/omw/input/playercontrols.lua
scripts/omw/input/actionbindings.lua
scripts/omw/input/smoothmovement.lua

@ -1,9 +1,10 @@
# UI framework
PLAYER: scripts/omw/mwui/init.lua
MENU,PLAYER: scripts/omw/mwui/init.lua
# Settings framework
GLOBAL: scripts/omw/settings/global.lua
MENU: scripts/omw/settings/menu.lua
PLAYER: scripts/omw/settings/player.lua
GLOBAL: scripts/omw/settings/global.lua
# Mechanics
GLOBAL: scripts/omw/activationhandlers.lua
@ -12,8 +13,10 @@ GLOBAL: scripts/omw/usehandlers.lua
GLOBAL: scripts/omw/worldeventhandlers.lua
CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua
PLAYER: scripts/omw/mechanics/playercontroller.lua
PLAYER: scripts/omw/playercontrols.lua
MENU: scripts/omw/camera/settings.lua
PLAYER: scripts/omw/camera/camera.lua
MENU: scripts/omw/input/settings.lua
PLAYER: scripts/omw/input/playercontrols.lua
PLAYER: scripts/omw/input/actionbindings.lua
PLAYER: scripts/omw/input/smoothmovement.lua
NPC,CREATURE: scripts/omw/ai.lua
@ -22,6 +25,7 @@ NPC,CREATURE: scripts/omw/ai.lua
PLAYER: scripts/omw/ui.lua
# Lua console
MENU: scripts/omw/console/menu.lua
PLAYER: scripts/omw/console/player.lua
GLOBAL: scripts/omw/console/global.lua
CUSTOM: scripts/omw/console/local.lua

@ -5,6 +5,7 @@ local util = require('openmw.util')
local self = require('openmw.self')
local nearby = require('openmw.nearby')
local async = require('openmw.async')
local storage = require('openmw.storage')
local I = require('openmw.interfaces')
local Actor = require('openmw.types').Actor
@ -28,7 +29,7 @@ input.registerAction {
defaultValue = 0,
}
local settings = require('scripts.omw.camera.settings').thirdPerson
local settings = storage.playerSection('SettingsOMWCameraThirdPerson')
local head_bobbing = require('scripts.omw.camera.head_bobbing')
local third_person = require('scripts.omw.camera.third_person')
local pov_auto_switch = require('scripts.omw.camera.first_person_auto_switch')

@ -2,12 +2,13 @@ local camera = require('openmw.camera')
local self = require('openmw.self')
local util = require('openmw.util')
local async = require('openmw.async')
local storage = require('openmw.storage')
local Actor = require('openmw.types').Actor
local M = {}
local settings = require('scripts.omw.camera.settings').headBobbing
local settings = storage.playerSection('SettingsOMWCameraHeadBobbing')
local doubleStepLength, stepHeight, maxRoll
@ -31,7 +32,7 @@ local arcHeight = sampleArc(1)
function M.update(dt, smoothedSpeed)
local speed = Actor.getCurrentSpeed(self)
speed = speed / (1 + speed / 500) -- limit bobbing frequency if the speed is very high
speed = speed / (1 + speed / 500) -- limit bobbing frequency if the speed is very high
totalMovement = totalMovement + speed * dt
if not M.enabled or camera.getMode() ~= camera.MODE.FirstPerson then
effectWeight = 0
@ -44,18 +45,17 @@ function M.update(dt, smoothedSpeed)
end
local doubleStepState = totalMovement / doubleStepLength
doubleStepState = doubleStepState - math.floor(doubleStepState) -- from 0 to 1 during 2 steps
local stepState = math.abs(doubleStepState * 4 - 2) - 1 -- from -1 to 1 on even steps and from 1 to -1 on odd steps
local effect = sampleArc(stepState) / arcHeight -- range from 0 to 1
doubleStepState = doubleStepState - math.floor(doubleStepState) -- from 0 to 1 during 2 steps
local stepState = math.abs(doubleStepState * 4 - 2) - 1 -- from -1 to 1 on even steps and from 1 to -1 on odd steps
local effect = sampleArc(stepState) / arcHeight -- range from 0 to 1
-- Smoothly reduce the effect to zero when the player stops
local coef = math.min(smoothedSpeed / 300, 1) * effectWeight
local zOffset = (0.5 - effect) * coef * stepHeight -- range from -stepHeight/2 to stepHeight/2
local roll = ((stepState > 0 and 1) or -1) * effect * coef * maxRoll -- range from -maxRoll to maxRoll
local zOffset = (0.5 - effect) * coef * stepHeight -- range from -stepHeight/2 to stepHeight/2
local roll = ((stepState > 0 and 1) or -1) * effect * coef * maxRoll -- range from -maxRoll to maxRoll
camera.setFirstPersonOffset(camera.getFirstPersonOffset() + util.vector3(0, 0, zOffset))
camera.setExtraRoll(camera.getExtraRoll() + roll)
end
return M

@ -3,10 +3,10 @@ local async = require('openmw.async')
local I = require('openmw.interfaces')
I.Settings.registerPage({
key = 'OMWCamera',
l10n = 'OMWCamera',
name = 'Camera',
description = 'settingsPageDescription',
key = 'OMWCamera',
l10n = 'OMWCamera',
name = 'Camera',
description = 'settingsPageDescription',
})
local thirdPersonGroup = 'SettingsOMWCameraThirdPerson'
@ -16,8 +16,8 @@ local function boolSetting(prefix, key, default)
return {
key = key,
renderer = 'checkbox',
name = prefix..key,
description = prefix..key..'Description',
name = prefix .. key,
description = prefix .. key .. 'Description',
default = default,
}
end
@ -26,8 +26,8 @@ local function floatSetting(prefix, key, default)
return {
key = key,
renderer = 'number',
name = prefix..key,
description = prefix..key..'Description',
name = prefix .. key,
description = prefix .. key .. 'Description',
default = default,
}
end
@ -70,33 +70,29 @@ I.Settings.registerGroup({
},
})
local settings = {
thirdPerson = storage.playerSection(thirdPersonGroup),
headBobbing = storage.playerSection(headBobbingGroup),
}
local thirdPerson = storage.playerSection(thirdPersonGroup)
local headBobbing = storage.playerSection(headBobbingGroup)
local function updateViewOverShoulderDisabled()
local shoulderDisabled = not settings.thirdPerson:get('viewOverShoulder')
I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetX', {disabled = shoulderDisabled})
I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetY', {disabled = shoulderDisabled})
I.Settings.updateRendererArgument(thirdPersonGroup, 'autoSwitchShoulder', {disabled = shoulderDisabled})
I.Settings.updateRendererArgument(thirdPersonGroup, 'zoomOutWhenMoveCoef', {disabled = shoulderDisabled})
local shoulderDisabled = not thirdPerson:get('viewOverShoulder')
I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetX', { disabled = shoulderDisabled })
I.Settings.updateRendererArgument(thirdPersonGroup, 'shoulderOffsetY', { disabled = shoulderDisabled })
I.Settings.updateRendererArgument(thirdPersonGroup, 'autoSwitchShoulder', { disabled = shoulderDisabled })
I.Settings.updateRendererArgument(thirdPersonGroup, 'zoomOutWhenMoveCoef', { disabled = shoulderDisabled })
local move360Disabled = not settings.thirdPerson:get('move360')
I.Settings.updateRendererArgument(thirdPersonGroup, 'move360TurnSpeed', {disabled = move360Disabled})
local move360Disabled = not thirdPerson:get('move360')
I.Settings.updateRendererArgument(thirdPersonGroup, 'move360TurnSpeed', { disabled = move360Disabled })
end
local function updateHeadBobbingDisabled()
local disabled = not settings.headBobbing:get('enabled')
I.Settings.updateRendererArgument(headBobbingGroup, 'step', {disabled = disabled, min = 1})
I.Settings.updateRendererArgument(headBobbingGroup, 'height', {disabled = disabled})
I.Settings.updateRendererArgument(headBobbingGroup, 'roll', {disabled = disabled, min = 0, max = 90})
local disabled = not headBobbing:get('enabled')
I.Settings.updateRendererArgument(headBobbingGroup, 'step', { disabled = disabled, min = 1 })
I.Settings.updateRendererArgument(headBobbingGroup, 'height', { disabled = disabled })
I.Settings.updateRendererArgument(headBobbingGroup, 'roll', { disabled = disabled, min = 0, max = 90 })
end
updateViewOverShoulderDisabled()
updateHeadBobbingDisabled()
settings.thirdPerson:subscribe(async:callback(updateViewOverShoulderDisabled))
settings.headBobbing:subscribe(async:callback(updateHeadBobbingDisabled))
return settings
thirdPerson:subscribe(async:callback(updateViewOverShoulderDisabled))
headBobbing:subscribe(async:callback(updateHeadBobbingDisabled))

@ -3,10 +3,11 @@ local util = require('openmw.util')
local self = require('openmw.self')
local nearby = require('openmw.nearby')
local async = require('openmw.async')
local storage = require('openmw.storage')
local Actor = require('openmw.types').Actor
local settings = require('scripts.omw.camera.settings').thirdPerson
local settings = storage.playerSection('SettingsOMWCameraThirdPerson')
local MODE = camera.MODE
local STATE = { RightShoulder = 0, LeftShoulder = 1, Combat = 2, Swimming = 3 }
@ -31,7 +32,7 @@ local function updateSettings()
viewOverShoulder = settings:get('viewOverShoulder')
autoSwitchShoulder = settings:get('autoSwitchShoulder')
shoulderOffset = util.vector2(settings:get('shoulderOffsetX'),
settings:get('shoulderOffsetY'))
settings:get('shoulderOffsetY'))
zoomOutWhenMoveCoef = settings:get('zoomOutWhenMoveCoef')
defaultShoulder = (shoulderOffset.x > 0 and STATE.RightShoulder) or STATE.LeftShoulder
@ -46,7 +47,7 @@ local state = defaultShoulder
local function ray(from, angle, limit)
local to = from + util.transform.rotateZ(angle) * util.vector3(0, limit, 0)
local res = nearby.castRay(from, to, {collisionType = camera.getCollisionType()})
local res = nearby.castRay(from, to, { collisionType = camera.getCollisionType() })
if res.hit then
return (res.hitPos - from):length()
else
@ -55,8 +56,8 @@ local function ray(from, angle, limit)
end
local function trySwitchShoulder()
local limitToSwitch = 120 -- switch to other shoulder if wall is closer than this limit
local limitToSwitchBack = 300 -- switch back to default shoulder if there is no walls at this distance
local limitToSwitch = 120 -- switch to other shoulder if wall is closer than this limit
local limitToSwitchBack = 300 -- switch back to default shoulder if there is no walls at this distance
local pos = camera.getTrackedPosition()
local rayRight = ray(pos, camera.getYaw() + math.rad(90), limitToSwitchBack + 1)
@ -79,7 +80,7 @@ end
local function calculateDistance(smoothedSpeed)
local smoothedSpeedSqr = smoothedSpeed * smoothedSpeed
return (M.baseDistance + math.max(camera.getPitch(), 0) * 50
+ smoothedSpeedSqr / (smoothedSpeedSqr + 300*300) * zoomOutWhenMoveCoef)
+ smoothedSpeedSqr / (smoothedSpeedSqr + 300 * 300) * zoomOutWhenMoveCoef)
end
local function updateState()
@ -95,7 +96,7 @@ local function updateState()
state = defaultShoulder
end
if (mode == MODE.ThirdPerson or Actor.getCurrentSpeed(self) > 0 or state ~= oldState or noThirdPersonLastFrame)
and (state == STATE.LeftShoulder or state == STATE.RightShoulder) then
and (state == STATE.LeftShoulder or state == STATE.RightShoulder) then
if autoSwitchShoulder then
trySwitchShoulder()
else
@ -108,11 +109,11 @@ local function updateState()
-- Player doesn't touch controls for a long time. Transition should be very slow.
camera.setFocalTransitionSpeed(0.2)
elseif (oldState == STATE.Combat or state == STATE.Combat) and
(mode ~= MODE.Preview or M.standingPreview) then
(mode ~= MODE.Preview or M.standingPreview) then
-- Transition to/from combat mode and we are not in preview mode. Should be fast.
camera.setFocalTransitionSpeed(5.0)
else
camera.setFocalTransitionSpeed(1.0) -- Default transition speed.
camera.setFocalTransitionSpeed(1.0) -- Default transition speed.
end
if state == STATE.RightShoulder then
@ -149,7 +150,7 @@ function M.update(dt, smoothedSpeed)
end
M.preferredDistance = calculateDistance(smoothedSpeed)
if noThirdPersonLastFrame then -- just switched to third person view
if noThirdPersonLastFrame then -- just switched to third person view
camera.setPreferredThirdPersonDistance(M.preferredDistance)
camera.instantTransition()
noThirdPersonLastFrame = false
@ -161,4 +162,3 @@ function M.update(dt, smoothedSpeed)
end
return M

@ -0,0 +1,115 @@
local menu = require('openmw.menu')
local ui = require('openmw.ui')
local util = require('openmw.util')
local menuModeName = 'Lua[Menu]'
local function printHelp()
local msg = [[
This is the built-in Lua interpreter.
help() - print this message
exit() - exit Lua mode
view(_G) - print content of the table `_G` (current environment)
standard libraries (math, string, etc.) are loaded by default but not visible in `_G`
view(menu, 2) - print table `menu` (i.e. `openmw.menu`) and its subtables (2 - traversal depth)]]
ui.printToConsole(msg, ui.CONSOLE_COLOR.Info)
end
local function printToConsole(...)
local strs = {}
for i = 1, select('#', ...) do
strs[i] = tostring(select(i, ...))
end
return ui.printToConsole(table.concat(strs, '\t'), ui.CONSOLE_COLOR.Info)
end
local function printRes(...)
if select('#', ...) >= 0 then
printToConsole(...)
end
end
local function exitLuaMenuMode()
ui.setConsoleMode('')
ui.printToConsole('Lua mode OFF', ui.CONSOLE_COLOR.Success)
end
local function enterLuaMenuMode()
ui.printToConsole('Lua mode ON, use exit() to return, help() for more info', ui.CONSOLE_COLOR.Success)
ui.printToConsole('Context: Menu', ui.CONSOLE_COLOR.Success)
ui.setConsoleMode(menuModeName)
end
local env = {
I = require('openmw.interfaces'),
menu = require('openmw.menu'),
util = require('openmw.util'),
core = require('openmw.core'),
storage = require('openmw.storage'),
vfs = require('openmw.vfs'),
ambient = require('openmw.ambient'),
async = require('openmw.async'),
ui = require('openmw.ui'),
input = require('openmw.input'),
aux_util = require('openmw_aux.util'),
view = require('openmw_aux.util').deepToString,
print = printToConsole,
exit = exitLuaMenuMode,
help = printHelp,
}
env._G = env
setmetatable(env, {__index = _G, __metatable = false})
_G = nil
local function executeLuaCode(code)
local fn
local ok, err = pcall(function() fn = util.loadCode('return ' .. code, env) end)
if ok then
ok, err = pcall(function() printRes(fn()) end)
else
ok, err = pcall(function() util.loadCode(code, env)() end)
end
if not ok then
ui.printToConsole(err, ui.CONSOLE_COLOR.Error)
end
end
local usageInfo = [[
Usage: 'lua menu' or 'luam' - enter menu context
Other contexts are available only when the game is started:
'lua player' or 'luap' - enter player context
'lua global' or 'luag' - enter global context
'lua selected' or 'luas' - enter local context on the selected object]]
local function onConsoleCommand(mode, cmd)
if mode == '' then
cmd, arg = cmd:lower():match('(%w+) *(%w*)')
if (cmd == 'lua' and arg == 'menu') or cmd == 'luam' then
enterLuaMenuMode()
elseif menu.getState() == menu.STATE.NoGame and (cmd == 'lua' or cmd == 'luap' or cmd == 'luas' or cmd == 'luag') then
ui.printToConsole(usageInfo, ui.CONSOLE_COLOR.Info)
end
elseif mode == menuModeName then
if cmd == 'exit()' then
exitLuaMenuMode()
else
executeLuaCode(cmd)
end
end
end
local function onStateChanged()
local mode = ui.getConsoleMode()
if menu.getState() ~= menu.STATE.Ended and mode ~= menuModeName then
-- When a new game started or loaded reset console mode (except of `luam`) because
-- other modes become invalid after restarting Lua scripts.
ui.setConsoleMode('')
end
end
return {
engineHandlers = {
onConsoleCommand = onConsoleCommand,
onStateChanged = onStateChanged,
},
}

@ -77,6 +77,7 @@ local env = {
nearby = require('openmw.nearby'),
self = require('openmw.self'),
input = require('openmw.input'),
postprocessing = require('openmw.postprocessing'),
ui = require('openmw.ui'),
camera = require('openmw.camera'),
aux_util = require('openmw_aux.util'),
@ -114,9 +115,12 @@ local function onConsoleCommand(mode, cmd, selectedObject)
cmd = 'luag'
elseif arg == 'selected' then
cmd = 'luas'
elseif arg == 'menu' then
-- handled in menu.lua
else
local msg = [[
Usage: 'lua player' or 'luap' - enter player context
Usage: 'lua menu' or 'luam' - enter menu context
'lua player' or 'luap' - enter player context
'lua global' or 'luag' - enter global context
'lua selected' or 'luas' - enter local context on the selected object]]
ui.printToConsole(msg, ui.CONSOLE_COLOR.Info)
@ -158,4 +162,3 @@ return {
OMWConsoleHelp = printHelp,
}
}

@ -134,90 +134,16 @@ function clearBinding(id)
end
end
local function updateBinding(id, binding)
bindingSection:set(id, binding)
bindingSection:subscribe(async:callback(function(_, id)
if not id then return end
local binding = bindingSection:get(id)
clearBinding(id)
if binding ~= nil then
registerBinding(binding, id)
end
return id
end
local interfaceL10n = core.l10n('interface')
I.Settings.registerRenderer('inputBinding', function(id, set, arg)
if type(id) ~= 'string' then error('inputBinding: must have a string default value') end
if not arg.type then error('inputBinding: type argument is required') end
if not arg.key then error('inputBinding: key argument is required') end
local info = input.actions[arg.key] or input.triggers[arg.key]
if not info then return {} end
local l10n = core.l10n(info.key)
local name = {
template = I.MWUI.templates.textNormal,
props = {
text = l10n(info.name),
},
}
local description = {
template = I.MWUI.templates.textNormal,
props = {
text = l10n(info.description),
},
}
local binding = bindingSection:get(id)
local label = binding and input.getKeyName(binding.code) or interfaceL10n('None')
local recorder = {
template = I.MWUI.templates.textEditLine,
props = {
readOnly = true,
text = label,
},
events = {
focusGain = async:callback(function()
if binding == nil then return end
updateBinding(id, nil)
set(id)
end),
keyPress = async:callback(function(key)
if binding ~= nil or key.code == input.KEY.Escape then return end
local newBinding = {
code = key.code,
type = arg.type,
key = arg.key,
}
updateBinding(id, newBinding)
set(id)
end),
},
}
local row = {
type = ui.TYPE.Flex,
props = {
horizontal = true,
},
content = ui.content {
name,
{ props = { size = util.vector2(10, 0) } },
recorder,
},
}
local column = {
type = ui.TYPE.Flex,
content = ui.content {
row,
description,
},
}
end))
return column
end)
local initiated = false

@ -9,38 +9,6 @@ local Player = require('openmw.types').Player
local I = require('openmw.interfaces')
local settingsGroup = 'SettingsOMWControls'
local function boolSetting(key, default)
return {
key = key,
renderer = 'checkbox',
name = key,
description = key .. 'Description',
default = default,
}
end
I.Settings.registerPage({
key = 'OMWControls',
l10n = 'OMWControls',
name = 'ControlsPage',
description = 'ControlsPageDescription',
})
I.Settings.registerGroup({
key = settingsGroup,
page = 'OMWControls',
l10n = 'OMWControls',
name = 'MovementSettings',
permanentStorage = true,
settings = {
boolSetting('alwaysRun', false),
boolSetting('toggleSneak', false), -- TODO: consider removing this setting when we have the advanced binding UI
boolSetting('smoothControllerMovement', true),
},
})
local settings = storage.playerSection('SettingsOMWControls')
do

@ -0,0 +1,136 @@
local core = require('openmw.core')
local input = require('openmw.input')
local storage = require('openmw.storage')
local ui = require('openmw.ui')
local util = require('openmw.util')
local async = require('openmw.async')
local I = require('openmw.interfaces')
local settingsGroup = 'SettingsOMWControls'
local function boolSetting(key, default)
return {
key = key,
renderer = 'checkbox',
name = key,
description = key .. 'Description',
default = default,
}
end
I.Settings.registerPage({
key = 'OMWControls',
l10n = 'OMWControls',
name = 'ControlsPage',
description = 'ControlsPageDescription',
})
I.Settings.registerGroup({
key = settingsGroup,
page = 'OMWControls',
l10n = 'OMWControls',
name = 'MovementSettings',
permanentStorage = true,
settings = {
boolSetting('alwaysRun', false),
boolSetting('toggleSneak', false), -- TODO: consider removing this setting when we have the advanced binding UI
boolSetting('smoothControllerMovement', true),
},
})
local interfaceL10n = core.l10n('interface')
local bindingSection = storage.playerSection('OMWInputBindings')
local recording = nil
I.Settings.registerRenderer('inputBinding', function(id, set, arg)
if type(id) ~= 'string' then error('inputBinding: must have a string default value') end
if not arg then error('inputBinding: argument with "key" and "type" is required') end
if not arg.type then error('inputBinding: type argument is required') end
if not arg.key then error('inputBinding: key argument is required') end
local info = input.actions[arg.key] or input.triggers[arg.key]
if not info then return {} end
local l10n = core.l10n(info.key)
local name = {
template = I.MWUI.templates.textNormal,
props = {
text = l10n(info.name),
},
}
local description = {
template = I.MWUI.templates.textNormal,
props = {
text = l10n(info.description),
},
}
local binding = bindingSection:get(id)
local label = interfaceL10n('None')
if binding then label = input.getKeyName(binding.code) end
if recording and recording.id == id then label = interfaceL10n('N/A') end
local recorder = {
template = I.MWUI.templates.textNormal,
props = {
text = label,
},
events = {
mouseClick = async:callback(function()
if recording ~= nil then return end
if binding ~= nil then bindingSection:set(id, nil) end
recording = {
id = id,
arg = arg,
refresh = function() set(id) end,
}
recording.refresh()
end),
},
}
local row = {
type = ui.TYPE.Flex,
props = {
horizontal = true,
},
content = ui.content {
name,
{ props = { size = util.vector2(10, 0) } },
recorder,
},
}
local column = {
type = ui.TYPE.Flex,
content = ui.content {
row,
description,
},
}
return column
end)
return {
engineHandlers = {
onKeyPress = function(key)
if recording == nil then return end
local binding = {
code = key.code,
type = recording.arg.type,
key = recording.arg.key,
}
if key.code == input.KEY.Escape then -- TODO: prevent settings modal from closing
binding.code = nil
end
bindingSection:set(recording.id, binding)
local refresh = recording.refresh
recording = nil
refresh()
end,
}
}

@ -6,8 +6,6 @@ local argumentSectionPostfix = 'Arguments'
local contextSection = storage.playerSection or storage.globalSection
local groupSection = contextSection(groupSectionKey)
groupSection:reset()
groupSection:removeOnExit()
local function validateSettingOptions(options)
if type(options) ~= 'table' then
@ -92,7 +90,6 @@ local function registerGroup(options)
}
local valueSection = contextSection(options.key)
local argumentSection = contextSection(options.key .. argumentSectionPostfix)
argumentSection:removeOnExit()
for i, opt in ipairs(options.settings) do
local setting = registerSetting(opt)
setting.order = i
@ -120,6 +117,7 @@ return {
argumentSection:set(settingKey, argument)
end,
setGlobalEvent = 'OMWSettingsGlobalSet',
registerPageEvent = 'OmWSettingsRegisterPage',
groupSectionKey = groupSectionKey,
onLoad = function(saved)
if not saved then return end

@ -1,10 +1,12 @@
local storage = require('openmw.storage')
local common = require('scripts.omw.settings.common')
common.getSection(true, common.groupSectionKey):removeOnExit()
return {
interfaceName = 'Settings',
interface = {
version = 1,
registerGroup = common.registerGroup,
updateRendererArgument = common.updateRendererArgument,
},
@ -17,4 +19,4 @@ return {
storage.globalSection(e.groupKey):set(e.settingKey, e.value)
end,
},
}
}

@ -1,3 +1,4 @@
local menu = require('openmw.menu')
local ui = require('openmw.ui')
local util = require('openmw.util')
local async = require('openmw.async')
@ -6,11 +7,14 @@ local storage = require('openmw.storage')
local I = require('openmw.interfaces')
local common = require('scripts.omw.settings.common')
-- :reset on startup instead of :removeOnExit
common.getSection(false, common.groupSectionKey):reset()
local renderers = {}
local function registerRenderer(name, renderFunction)
renderers[name] = renderFunction
end
require('scripts.omw.settings.renderers')(registerRenderer)
local interfaceL10n = core.l10n('Interface')
@ -25,12 +29,12 @@ local growingIntreval = {
grow = 1,
},
}
local spacer = {
local spacer = {
props = {
size = util.vector2(0, 10),
},
}
local bigSpacer = {
local bigSpacer = {
props = {
size = util.vector2(0, 50),
},
@ -45,7 +49,7 @@ local spacedLines = function(count)
local content = {}
table.insert(content, spacer)
table.insert(content, stretchingLine)
for i = 2, count do
for _ = 2, count do
table.insert(content, interval)
table.insert(content, stretchingLine)
end
@ -267,11 +271,16 @@ end
local function renderPage(page)
local l10n = core.l10n(page.l10n)
local sortedGroups = {}
for i, v in ipairs(groups[page.key]) do sortedGroups[i] = v end
for _, group in pairs(groups[page.key]) do
table.insert(sortedGroups, group)
end
table.sort(sortedGroups, pageGroupComparator)
local groupLayouts = {}
for _, pageGroup in ipairs(sortedGroups) do
local group = common.getSection(pageGroup.global, common.groupSectionKey):get(pageGroup.key)
if not group then
error(string.format('%s group "%s" was not found', pageGroup.global and 'Global' or 'Player', pageGroup.key))
end
table.insert(groupLayouts, renderGroup(group, pageGroup.global))
end
local groupsLayout = {
@ -342,52 +351,99 @@ local function onSettingChanged(global)
element:update()
end)
end
local function onGroupRegistered(global, key)
local group = common.getSection(global, common.groupSectionKey):get(key)
if not group then return end
groups[group.page] = groups[group.page] or {}
local pageGroup = {
local pageGroup = {
key = group.key,
global = global,
order = group.order,
}
table.insert(groups[group.page], pageGroup)
common.getSection(global, group.key):subscribe(onSettingChanged(global))
common.getArgumentSection(global, group.key):subscribe(async:callback(function(_, settingKey)
local groupKey = group.key
local group = common.getSection(global, common.groupSectionKey):get(groupKey)
if not group or not pageOptions[group.page] then return end
local value = common.getSection(global, group.key):get(settingKey)
if not groups[group.page][pageGroup.key] then
common.getSection(global, group.key):subscribe(onSettingChanged(global))
common.getArgumentSection(global, group.key):subscribe(async:callback(function(_, settingKey)
if settingKey == nil then return end
local element = pageOptions[group.page].element
local groupsLayout = element.layout.content.groups
local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)]
local settingsContent = groupLayout.content.settings.content
settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global)
element:update()
end))
local group = common.getSection(global, common.groupSectionKey):get(group.key)
if not group or not pageOptions[group.page] then return end
local value = common.getSection(global, group.key):get(settingKey)
local element = pageOptions[group.page].element
local groupsLayout = element.layout.content.groups
local groupLayout = groupsLayout.content[groupLayoutName(group.key, global)]
local settingsContent = groupLayout.content.settings.content
settingsContent[settingKey] = renderSetting(group, group.settings[settingKey], value, global)
element:update()
end))
end
groups[group.page][pageGroup.key] = pageGroup
if not pages[group.page] then return end
local options = renderPage(pages[group.page])
if pageOptions[group.page] then
pageOptions[group.page].element:destroy()
else
pageOptions[group.page] = {}
end
for k, v in pairs(options) do
local renderedOptions = renderPage(pages[group.page])
for k, v in pairs(renderedOptions) do
pageOptions[group.page][k] = v
end
end
local globalGroups = storage.globalSection(common.groupSectionKey)
for groupKey in pairs(globalGroups:asTable()) do
onGroupRegistered(true, groupKey)
local function updateGroups(global)
local groupSection = common.getSection(global, common.groupSectionKey)
for groupKey in pairs(groupSection:asTable()) do
onGroupRegistered(global, groupKey)
end
groupSection:subscribe(async:callback(function(_, key)
if key then
onGroupRegistered(global, key)
else
for groupKey in pairs(groupSection:asTable()) do
onGroupRegistered(global, groupKey)
end
end
end))
end
local updatePlayerGroups = function() updateGroups(false) end
updatePlayerGroups()
local updateGlobalGroups = function() updateGroups(true) end
local menuGroups = {}
local menuPages = {}
local function resetPlayerGroups()
print('MENU reset player groups')
local playerGroupsSection = storage.playerSection(common.groupSectionKey)
for pageKey, page in pairs(groups) do
for groupKey, group in pairs(page) do
if not menuGroups[groupKey] and not group.global then
page[groupKey] = nil
playerGroupsSection:set(groupKey, nil)
end
end
if pageOptions[pageKey] then
pageOptions[pageKey].element:destroy()
if not menuPages[pageKey] then
ui.removeSettingsPage(pageOptions[pageKey])
pageOptions[pageKey] = nil
else
local renderedOptions = renderPage(pages[pageKey])
for k, v in pairs(renderedOptions) do
pageOptions[pageKey][k] = v
end
end
end
end
end
globalGroups:subscribe(async:callback(function(_, key)
if key then onGroupRegistered(true, key) end
end))
storage.playerSection(common.groupSectionKey):subscribe(async:callback(function(_, key)
if key then onGroupRegistered(false, key) end
end))
local function registerPage(options)
if type(options) ~= 'table' then
@ -413,11 +469,48 @@ local function registerPage(options)
}
pages[page.key] = page
groups[page.key] = groups[page.key] or {}
pageOptions[page.key] = renderPage(page)
if pageOptions[page.key] then
pageOptions[page.key].element:destroy()
end
pageOptions[page.key] = pageOptions[page.key] or {}
local renderedOptions = renderPage(page)
for k, v in pairs(renderedOptions) do
pageOptions[page.key][k] = v
end
ui.registerSettingsPage(pageOptions[page.key])
end
return {
registerPage = registerPage,
registerRenderer = registerRenderer,
}
interfaceName = 'Settings',
interface = {
version = 1,
registerPage = function(options)
registerPage(options)
menuPages[options.key] = true
end,
registerRenderer = registerRenderer,
registerGroup = function(options)
if not options.permanentStorage then
error('Menu scripts are only allowed to register setting groups with permanentStorage = true')
end
common.registerGroup(options)
menuGroups[options.key] = true
end,
updateRendererArgument = common.updateRendererArgument,
},
engineHandlers = {
onStateChanged = function()
if menu.getState() == menu.STATE.Running then
updatePlayerGroups()
updateGlobalGroups()
else
resetPlayerGroups()
end
end,
},
eventHandlers = {
[common.registerPageEvent] = function(options)
registerPage(options)
end,
}
}

@ -1,7 +1,11 @@
local types = require('openmw.types')
local self = require('openmw.self')
local common = require('scripts.omw.settings.common')
local render = require('scripts.omw.settings.render')
require('scripts.omw.settings.renderers')(render.registerRenderer)
local function registerPage(options)
types.Player.sendMenuEvent(self, common.registerPageEvent, options)
end
---
-- @type PageOptions
@ -71,11 +75,11 @@ return {
-- local globalSettings = storage.globalSection('SettingsGlobalMyMod')
interface = {
---
-- @field [parent=#Settings] #string version
version = 0,
-- @field [parent=#Settings] #number version
version = 1,
---
-- @function [parent=#Settings] registerPage Register a page to be displayed in the settings menu,
-- only available in player scripts
-- available in player and menu scripts
-- @param #PageOptions options
-- @usage
-- I.Settings.registerPage({
@ -84,10 +88,10 @@ return {
-- name = 'MyModName',
-- description = 'MyModDescription',
-- })---
registerPage = render.registerPage,
registerPage = registerPage,
---
-- @function [parent=#Settings] registerRenderer Register a renderer,
-- only avaialable in player scripts
-- only available in menu scripts (DEPRECATED in player scripts)
-- @param #string key
-- @param #function renderer A renderer function, receives setting's value,
-- a function to change it and an argument from the setting options
@ -107,10 +111,14 @@ return {
-- },
-- }
-- end)
registerRenderer = render.registerRenderer,
registerRenderer = function(name)
print(([[Can't register setting renderer "%s". registerRenderer and moved to Menu context Settings interface]])
:format(name))
end,
---
-- @function [parent=#Settings] registerGroup Register a group to be attached to a page,
-- available both in player and global scripts
-- available in player, menu and global scripts
-- Note: menu scripts only allow group with permanentStorage = true, but can render the page before a game is loaded!
-- @param #GroupOptions options
-- @usage
-- I.Settings.registerGroup {
@ -140,7 +148,7 @@ return {
registerGroup = common.registerGroup,
---
-- @function [parent=#Settings] updateRendererArgument Change the renderer argument of a setting
-- available both in player and global scripts
-- available both in player, menu and global scripts
-- @param #string groupKey A settings group key
-- @param #string settingKey A setting key
-- @param argument A renderer argument

@ -21,6 +21,7 @@ set(LUA_API_FILES
openmw/util.lua
openmw/vfs.lua
openmw/world.lua
openmw/menu.lua
)
foreach (f ${LUA_API_FILES})

@ -1,6 +1,6 @@
---
-- `openmw.ambient` controls background sounds, specific to given player (2D-sounds).
-- Can be used only by local scripts, that are attached to a player.
-- Can be used only by menu scripts and local scripts, that are attached to a player.
-- @module ambient
-- @usage local ambient = require('openmw.ambient')

@ -1,6 +1,6 @@
---
-- `openmw.core` defines functions and types that are available in both local
-- and global scripts.
-- `openmw.core` defines functions and types that are available in local,
-- global and menu scripts.
-- @module core
-- @usage local core = require('openmw.core')

@ -1,5 +1,5 @@
---
-- `openmw.input` can be used only in scripts attached to a player.
-- `openmw.input` can be used only in menu scripts and scripts attached to a player.
-- @module input
-- @usage local input = require('openmw.input')

@ -0,0 +1,72 @@
---
-- `openmw.menu` can be used only in menu scripts.
-- @module menu
-- @usage local menu = require('openmw.menu')
---
-- @type STATE
-- @field [parent=#STATE] NoGame
-- @field [parent=#STATE] Running
-- @field [parent=#STATE] Ended
---
-- All possible game states returned by @{#menu.getState}
-- @field [parent=#menu] #STATE STATE
---
-- Current game state
-- @function [parent=#menu] getState
-- @return #STATE
---
-- Start a new game
-- @function [parent=#menu] newGame
---
-- Load the game from a save slot
-- @function [parent=#menu] loadGame
-- @param #string directory name of the save directory (e. g. character)
-- @param #string slotName name of the save slot
---
-- Delete a saved game
-- @function [parent=#menu] deleteGame
-- @param #string directory name of the save directory (e. g. character)
-- @param #string slotName name of the save slot
---
-- Current save directory
-- @function [parent=#menu] getCurrentSaveDir
-- @return #string
---
-- Save the game
-- @function [parent=#menu] saveGame
-- @param #string description human readable description of the save
-- @param #string slotName name of the save slot
---
-- @type SaveInfo
-- @field #string description
-- @field #string playerName
-- @field #string playerLevel
-- @field #number timePlayed Gameplay time for this saved game. Note: available even with [time played](../modding/settings/saves.html#timeplayed) turned off
-- @field #number creationTime Time at which the game was saved, as a timestamp in seconds. Can be passed as the second argument to `os.data`.
-- @field #list<#string> contentFiles
---
-- List of all saves for the given directory
-- @function [parent=#menu] getSaves
-- @param #string directory name of the save directory (e. g. character)
-- @return #list<#SaveInfo>
---
-- List of all available saves, grouped by directory
-- @function [parent=#menu] getAllSaves
-- @return #map<#string, #list<#SaveInfo>>
---
-- Exit the game
-- @function [parent=#menu] quit
return nil

@ -17,13 +17,14 @@
---
-- Get a section of the global storage; can be used by any script, but only global scripts can change values.
-- Menu scripts can only access it when a game is running.
-- Creates the section if it doesn't exist.
-- @function [parent=#storage] globalSection
-- @param #string sectionName
-- @return #StorageSection
---
-- Get a section of the player storage; can be used by player scripts only.
-- Get a section of the player storage; can only be used by player and menu scripts.
-- Creates the section if it doesn't exist.
-- @function [parent=#storage] playerSection
-- @param #string sectionName
@ -36,7 +37,7 @@
-- @return #table
---
-- Get all global sections as a table; can be used by player scripts only.
-- Get all player sections as a table; can only be used by player and menu scripts.
-- Note that adding/removing items to the returned table doesn't create or remove sections.
-- @function [parent=#storage] allPlayerSections
-- @return #table

@ -1045,6 +1045,12 @@
-- Values that can be used with getControlSwitch/setControlSwitch.
-- @field [parent=#Player] #CONTROL_SWITCH CONTROL_SWITCH
---
-- Send an event to menu scripts.
-- @function [parent=#core] sendMenuEvent
-- @param openmw.core#GameObject player
-- @param #string eventName
-- @param eventData
--------------------------------------------------------------------------------
-- @{#Armor} functions

@ -1,6 +1,6 @@
---
-- `openmw.ui` controls user interface.
-- Can be used only by local scripts, that are attached to a player.
-- Can be used only by menu scripts and local scripts, that are attached to a player.
-- @module ui
-- @usage
-- local ui = require('openmw.ui')
@ -93,6 +93,11 @@
-- @function [parent=#ui] registerSettingsPage
-- @param #SettingsPageOptions page
---
-- Removes the settings page
-- @function [parent=#ui] removeSettingsPage
-- @param #SettingsPageOptions page must be the exact same table of options as the one passed to registerSettingsPage
---
-- Table with settings page options, passed as an argument to ui.registerSettingsPage
-- @type SettingsPageOptions

Loading…
Cancel
Save