Skip to content

Segment Lua runtime authority - #4

Open
magicaldave wants to merge 12 commits into
DREAM-BIRTH-26from
DREAM/fix/lua-segmentation
Open

Segment Lua runtime authority#4
magicaldave wants to merge 12 commits into
DREAM-BIRTH-26from
DREAM/fix/lua-segmentation

Conversation

@magicaldave

@magicaldave magicaldave commented Jun 21, 2026

Copy link
Copy Markdown
Member

Dreamweave Lua Runtime Authority Segmentation

Summary

This PR establishes explicit Lua runtime authority boundaries for Dreamweave networking work.

The important split is:

  • Authoritative Lua runtime owns GLOBAL and LOAD scripts.
  • Client Lua runtime owns PLAYER, LOCAL, MENU, and CUSTOM scripts.
  • CUSTOM scripts are client-local explicit-attachment scripts. They are not authority-neutral.

Host mode now creates both an authoritative Lua runtime and a client Lua runtime. Dedicated server creates only the authoritative runtime. Client mode creates only the client runtime.

This is intentionally not a Lua thread-pool branch. Server/authoritative Lua remains single-threaded. Host and Client keep the existing client Lua worker behavior for the client runtime only.

What Changed

Runtime identity

  • Added explicit Lua runtime mode:
    • AuthoritativeServer
    • Client
  • Lua configuration and package selection now derive from runtime mode instead of loose dedicated/client checks.
  • Environment now has explicit authoritative/client Lua manager accessors.

Script filtering

  • Authoritative runtime receives only GLOBAL and LOAD scripts.
  • Client runtime receives only PLAYER, LOCAL, MENU, and CUSTOM scripts.
  • Mixed authoritative/client-local script declarations are rejected.

Package and storage ownership

  • LOAD now belongs to the authoritative runtime and uses authoritative/global storage.
  • Client runtime does not start global scripts or activate fake client-side global storage.
  • Client-side global storage replication is not implemented in this PR.

Event authority boundary

  • openmw.core.sendGlobalEvent now routes by Lua context authority:
    • authoritative contexts queue directly into authoritative global event handling;
    • client contexts route through LuaEventRouter to the authoritative runtime or network path.
  • Networked global event ingress now goes through a named router boundary instead of inline server/Lua manager policy.

Host dual runtime

  • Host creates both authoritative and client Lua managers.
  • Authoritative Lua updates on the main thread.
  • Client Lua keeps existing worker behavior.
  • Lifecycle/update ordering is authoritative-first where both runtimes must observe a transition.

Local script ownership

  • Authoritative Lua is guarded from creating/loading/saving/updating client local script containers.
  • RefData::getLuaScripts() / LocalScripts / PlayerScripts are client-local ownership seams.
  • Global object script APIs that would directly inspect/mutate local script containers are blocked for now.

Explicitly Deferred

Script-id mapping / script registry transfer

The full script-id mapping story is deferred.

Local script save/load still depends on script registry metadata that historically lived in the single LUAM path. With split runtimes, the authoritative runtime owns global LUAM data while the client runtime owns local script containers.

The next branch must define how authoritative script registry/config metadata is delivered to clients before local script state is deserialized. The likely model is:

  1. authoritative runtime owns canonical script registry / script-id mapping;
  2. server sends that mapping to clients on connect/load;
  3. client runtime uses it to deserialize local/player/custom script state safely.

This PR does not implement that network transfer.

Global-to-local script mutation APIs

APIs such as global-context local script attach/remove should eventually route through an explicit authority/client-local boundary.

They should not directly mutate RefData::getLuaScripts() from authoritative/global contexts.

Deferred future shape:

global script request
  -> authoritative boundary/validation
  -> client-local mutation command
  -> client runtime mutates LocalScripts

This PR blocks direct local container mutation where it would violate runtime ownership, but does not implement the future boundary-routed semantics.

Client global storage replication

Client local/player/menu scripts do not get a real replicated global storage view in this PR.

If client-side APIs attempt to access global storage before a replication model exists, failure is preferable to silently writing fake client-local global state.

Future work must define global storage serialization/replication semantics.

LuaManager decomposition

This PR exposes that LuaManager is too broad. It currently owns runtime state, script configuration, packages, events, storage, serialization, local script factories, and engine lifecycle handling.

Future work should split responsibilities into smaller components, likely along these lines:

  • runtime / Lua state owner;
  • authoritative global/load runtime;
  • client menu/local/player runtime;
  • script registry / script-id mapping;
  • save/load serialization bridge;
  • cross-runtime/network event boundary.

Not In Scope

  • Lua thread pool implementation.
  • Server Lua parallelism.
  • Client local script sharding.
  • Host server thread.
  • Transport abstraction.
  • Replication model.
  • Full script registry transfer.
  • Global storage replication.
  • Complete boundary-routed global-to-local mutation API.

Testing Checklist

Before merging, test at minimum:

  • Host startup with GLOBAL, LOAD, PLAYER, LOCAL, MENU, and CUSTOM scripts present.
  • Dedicated server startup with GLOBAL and LOAD scripts only.
  • Client mode startup with no authoritative Lua manager.
  • sendGlobalEvent from:
    • GLOBAL;
    • LOAD;
    • MENU;
    • PLAYER;
    • LOCAL.
  • Host scene load/unload and exterior creation with global handlers active.
  • Teleport paths from gameplay, MWScript, and Lua.
  • Save/load with global storage and local script containers.
  • reloadlua from MWScript console.
  • Lua debug reloadLua from a client/local context.

Known Risk

This branch changes Lua ownership semantics substantially. It is intended as authority-boundary foundation work, not as a final multiplayer scripting model.

The branch should be reviewed with special attention to call sites that still use transitional getLuaManager() access. Where authority matters, call sites should use explicit authoritative/client accessors or a named boundary.

Summary by CodeRabbit

Release Notes

  • New Features

    • Separate Lua runtime management for authoritative servers and clients with independent script lifecycle and event handling.
    • Script filtering by runtime mode (All, AuthoritativeServer, Client) during initialization.
    • Improved event routing system for networked global events.
  • Bug Fixes

    • Fixed client-side operations (input, UI, dialogue) now correctly target the client Lua runtime.
    • Enhanced validation of script runtime category compatibility.
  • Tests

    • Added test coverage for runtime filter initialization and mixed-authority script validation.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Splits the single LuaManager into separate authoritative-server and client Lua managers. Introduces a RuntimeFilter enum in ScriptsConfiguration, a RuntimeMode enum in LuaManager, and a new MWBase::LuaEventRouter interface implemented by MWNet::LuaEventRouter for cross-runtime global event routing. Engine conditionally constructs each manager by runtime role, and all engine, world, input, GUI, dialogue, network, and script call sites are migrated to target the correct manager.

Changes

Dual Authoritative/Client Lua Runtime Architecture

Layer / File(s) Summary
Runtime taxonomy contracts: RuntimeFilter, RuntimeMode, LuaEventRouter interface
components/lua/configuration.hpp, apps/openmw/mwbase/luamanager.hpp, apps/openmw/mwbase/luaeventrouter.hpp, apps/openmw/mwlua/context.hpp
ScriptsConfiguration::InitOptions replaces mGlobalOnly with a RuntimeFilter enum; LuaManager gains RuntimeMode and isAuthoritativeServer(); a new MWBase::LuaEventRouter pure-virtual interface declares the two cross-runtime event routing methods; Context::Type gains Common and isAuthoritative().
ScriptsConfiguration runtime validation and filtering
components/lua/configuration.cpp, apps/components_tests/lua/testconfiguration.cpp
Adds runtime-authority classification helpers, validates that scripts do not mix authoritative and client-local categories, re-validates merged duplicates, gates final inclusion via isAllowedByRuntimeFilter, and tests AuthoritativeServer/Client selection plus rejection of mixed-authority entries.
Environment multi-manager API
apps/openmw/mwbase/environment.hpp, apps/openmw/mwbase/environment.cpp
Rewrites MWBase::Environment to hold three optional Lua manager references (transitional, authoritative, client) and a LuaEventRouter*; adds presence checks, throwing getters, clearLuaManagers, getLuaManagerForGlobalScripts, forEachLuaManagerAuthoritativeFirst, and reloadAllLuaManagersAuthoritativeFirst.
LuaManager runtime-mode ownership and guarded behavior
apps/openmw/mwlua/luamanagerimp.hpp, apps/openmw/mwlua/luamanagerimp.cpp, apps/openmw/mwlua/luaevents.hpp, apps/openmw/mwlua/luaevents.cpp, apps/openmw/mwlua/engineevents.hpp, apps/openmw/mwlua/engineevents.cpp
LuaManager accepts RuntimeMode, derives ownsAuthoritativeScriptContexts/ownsClientScriptContexts, and guards every handler (update, lifecycle, storage, console, scene, teleport, scripts) behind ownership checks. LuaEvents and EngineEvents gain localEventsEnabled/localScriptsEnabled flags propagated from ownership.
MWNet::LuaEventRouter implementation and NetworkManager wiring
apps/openmw/mwnet/luaeventrouter.hpp, apps/openmw/mwnet/luaeventrouter.cpp, apps/openmw/mwnet/networkmanager.hpp, apps/openmw/mwnet/server.hpp, apps/openmw/mwnet/client.hpp
Implements MWNet::LuaEventRouter: inbound path validates the authoritative manager and forwards via queueNetworkedGlobalEvent; outbound path enqueues directly or wraps into GlobalEventDataMessageEntry for remote dispatch. NetworkManager gains a router member/accessor; Server routes GlobalEventQueuedMessage through the router.
Engine dual-manager creation and frame loop
apps/openmw/engine.hpp, apps/openmw/engine.cpp, apps/openmw/CMakeLists.txt
Engine replaces mLuaManager with mAuthoritativeLuaManager/mClientLuaManager, conditionally constructs each by runtime role (Host/DedicatedServer/Client), wires the LuaEventRouter from NetworkManager, splits synchronizedUpdate/update/worker per role, and broadcasts init/shutdown/storage calls via authoritative-first iteration.
Lua sendGlobalEvent routing via context.isAuthoritative()
apps/openmw/mwlua/corebindings.cpp, apps/openmw/mwlua/debugbindings.cpp
corebindings.cpp replaces dedicated-server detection with context.isAuthoritative(): authoritative scripts enqueue via addGlobalEvent; non-authoritative forward via getLuaEventRouter()->sendGlobalEventToAuthoritativeRuntime. debugbindings reloadLua uses context.mLuaManager directly.
StateManager and World lifecycle migration
apps/openmw/mwstate/statemanagerimp.cpp, apps/openmw/mwworld/worldimp.cpp, apps/openmw/mwworld/worldmodel.cpp, apps/openmw/mwworld/scene.cpp, apps/openmw/mwworld/actionteleport.cpp, apps/openmw/mwworld/livecellref.cpp, apps/openmw/mwlua/objectbindings.cpp, apps/openmw/mwscript/transformationextensions.cpp
StateManager and World broadcast all lifecycle events (cleanup, load/save, teleport, scene add/remove, player setup, new game) via forEachLuaManagerAuthoritativeFirst; save/load record access uses getLuaManagerForGlobalScripts. GObject hasScript/removeScript bindings updated to reflect no local script container ownership.
Input, GUI, dialogue, and script call site migration
apps/openmw/mwinput/..., apps/openmw/mwgui/..., apps/openmw/mwdialogue/dialoguemanagerimp.cpp, apps/openmw/mwscript/miscextensions.cpp, apps/openmw/mwlua/types/player.cpp
All keyboard, mouse, controller, action, GUI (console, debug, inventory, settings, window manager), and dialogue call sites switch from getLuaManager() to getClientLuaManager(). OpReloadLua uses reloadAllLuaManagersAuthoritativeFirst. Player control-switch bindings replace NetworkManager::isDedicatedServer() with isAuthoritativeServer().

Sequence Diagram(s)

sequenceDiagram
  participant Engine
  participant AuthLuaMgr as Authoritative LuaManager
  participant ClientLuaMgr as Client LuaManager
  participant LuaEventRouter as MWNet::LuaEventRouter
  participant NetworkServer as mwnet::Server

  rect rgba(70, 130, 180, 0.5)
    note over Engine: prepareEngine (Host role)
    Engine->>AuthLuaMgr: new LuaManager(RuntimeMode::AuthoritativeServer)
    Engine->>ClientLuaMgr: new LuaManager(RuntimeMode::Client)
    Engine->>LuaEventRouter: setLuaEventRouter(networkManager.getLuaEventRouter())
  end

  rect rgba(60, 179, 113, 0.5)
    note over ClientLuaMgr: Client script calls sendGlobalEvent
    ClientLuaMgr->>LuaEventRouter: sendGlobalEventToAuthoritativeRuntime(name, data)
    alt same process (Host)
      LuaEventRouter->>AuthLuaMgr: queueNetworkedGlobalEvent(name, data)
    else remote (Client→DedicatedServer)
      LuaEventRouter->>NetworkServer: queueMessage(GlobalEventDataMessageEntry)
      NetworkServer->>LuaEventRouter: receiveNetworkedGlobalEvent(clientIdx, name, data)
      LuaEventRouter->>AuthLuaMgr: queueNetworkedGlobalEvent(name, data)
    end
  end

  rect rgba(188, 143, 143, 0.5)
    note over Engine: frame()
    Engine->>AuthLuaMgr: synchronizedUpdate()
    Engine->>ClientLuaMgr: synchronizedUpdate()
    Engine->>AuthLuaMgr: update()
    Engine->>ClientLuaMgr: allowUpdate() via LuaWorker thread
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • DreamWeave-MP/DreamWeave#2: Modifies ScriptsConfiguration::init options and call sites in components/lua/configuration.*, directly overlapping with this PR's replacement of mGlobalOnly with RuntimeFilter.

Poem

🐇 Hop, hop, two managers now roam,
Authoritative server holds the tome,
The client scripts dance on their own stage,
Events route through the router's page,
No more single manager bears the load —
Each runtime hops its separate road! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Segment Lua runtime authority' clearly and concisely summarizes the main change: establishing separate Lua runtime authority boundaries between authoritative and client runtimes. This matches the core objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DREAM/fix/lua-segmentation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/openmw/mwbase/environment.hpp`:
- Around line 157-158: The getLuaManager() method has a return type of
Misc::NotNullPtr<LuaManager> which guarantees a non-null return value, but the
current implementation can return nullptr when mLuaManager is null or falsy in
the ternary expression. Fix this by throwing an exception when mLuaManager is
unset, similar to how getAuthoritativeLuaManager() and getClientLuaManager() are
implemented. Replace the ternary operator with a check that throws when the
value is not set, ensuring the method's non-null contract is upheld.

In `@apps/openmw/mwlua/engineevents.cpp`:
- Around line 23-25: The Visitor constructor currently only receives the
localScriptsEnabled flag, but the OnActive handler relies on
NetworkManager::isDedicatedServer() to route player-active events to global
scripts, which fails in host mode where the authoritative Lua runtime is not a
dedicated server. Modify the Visitor constructor to accept an additional
parameter indicating Lua authority (whether this runtime is authoritative for
global scripts), store it as a member variable alongside mGlobalScripts and
mLocalScriptsEnabled, and then update the OnActive handler to use this authority
flag instead of isDedicatedServer() when determining whether to call
mGlobalScripts.playerAdded(...).

In `@apps/openmw/mwlua/luaevents.cpp`:
- Around line 38-54: The local event batches (mLocalEventBatch and
mNewLocalEventBatch) in the LuaEvents class are not being serialized when the
game saves, causing pending local events to be lost. You need to add
serialization and deserialization handling for these local event batches,
similar to how global scripts are handled with
getLuaManagerForGlobalScripts()->write(). Add write() and read() methods (or
update existing save/load mechanisms) in the LuaEvents class to persist
mLocalEventBatch and mNewLocalEventBatch to the save file and restore them on
load, ensuring local events queued just before saving are preserved across game
sessions.

In `@apps/openmw/mwlua/luamanagerimp.cpp`:
- Around line 376-380: The reloadAllScripts() function is calling
reloadAllScriptsImpl() immediately, which causes reentrancy issues when
Lua-facing reload paths execute during the authoritative Lua runtime. Instead of
directly invoking reloadAllScriptsImpl(), set a flag to defer the reload
operation and service it later in the authoritative server's control flow.
Specifically, set the reload flag unconditionally in reloadAllScripts() (for
both runtimes), and then check and execute the actual reload in the
isAuthoritativeServer() branch after calling applyDelayedActions() to ensure it
happens at the safe point after synchronizedUpdate completes. Apply this same
pattern to the additional locations mentioned at lines 924-929.

In `@apps/openmw/mwlua/objectbindings.cpp`:
- Around line 418-421: The hasScript lambda function returns false
unconditionally for global objects, but this is inconsistent with the
removeScript lambda which throws an exception explaining the authority boundary
issue. Update the hasScript lambda (at objectT["hasScript"]) to throw the same
std::runtime_error exception with an appropriate message explaining that global
object scripts cannot be accessed due to authority boundary restrictions, rather
than silently returning false, so that Lua code receives clear feedback about
why the operation cannot be performed.

In `@apps/openmw/mwnet/luaeventrouter.cpp`:
- Around line 17-23: The issue is that the LuaEventRouter is dropping valid
networked global events during the transitional Host state when the
authoritative Lua manager has not yet been registered (the early return in the
environment.hasAuthoritativeLuaManager() check). Instead of immediately
returning and discarding the event, implement a buffering mechanism to queue
these events and flush them once the authoritative Lua manager becomes
available, or alternatively implement an explicit NACK or retry mechanism to
ensure events are redelivered. Modify the logic around the
environment.hasAuthoritativeLuaManager() check to cache incoming events in a
queue during the bootstrap phase, then process and clear that queue once the
authoritative manager registers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d0be1e4d-ef15-4a54-a355-dcdaba5f35e2

📥 Commits

Reviewing files that changed from the base of the PR and between e2bcde5 and 358b011.

📒 Files selected for processing (44)
  • apps/components_tests/lua/testconfiguration.cpp
  • apps/openmw/CMakeLists.txt
  • apps/openmw/engine.cpp
  • apps/openmw/engine.hpp
  • apps/openmw/mwbase/environment.cpp
  • apps/openmw/mwbase/environment.hpp
  • apps/openmw/mwbase/luaeventrouter.hpp
  • apps/openmw/mwbase/luamanager.hpp
  • apps/openmw/mwdialogue/dialoguemanagerimp.cpp
  • apps/openmw/mwgui/console.cpp
  • apps/openmw/mwgui/debugwindow.cpp
  • apps/openmw/mwgui/inventorywindow.cpp
  • apps/openmw/mwgui/settingswindow.cpp
  • apps/openmw/mwgui/windowmanagerimp.cpp
  • apps/openmw/mwinput/actionmanager.cpp
  • apps/openmw/mwinput/controllermanager.cpp
  • apps/openmw/mwinput/keyboardmanager.cpp
  • apps/openmw/mwinput/mousemanager.cpp
  • apps/openmw/mwlua/context.hpp
  • apps/openmw/mwlua/corebindings.cpp
  • apps/openmw/mwlua/debugbindings.cpp
  • apps/openmw/mwlua/engineevents.cpp
  • apps/openmw/mwlua/engineevents.hpp
  • apps/openmw/mwlua/luaevents.cpp
  • apps/openmw/mwlua/luaevents.hpp
  • apps/openmw/mwlua/luamanagerimp.cpp
  • apps/openmw/mwlua/luamanagerimp.hpp
  • apps/openmw/mwlua/objectbindings.cpp
  • apps/openmw/mwlua/types/player.cpp
  • apps/openmw/mwnet/client.hpp
  • apps/openmw/mwnet/luaeventrouter.cpp
  • apps/openmw/mwnet/luaeventrouter.hpp
  • apps/openmw/mwnet/networkmanager.hpp
  • apps/openmw/mwnet/server.hpp
  • apps/openmw/mwscript/miscextensions.cpp
  • apps/openmw/mwscript/transformationextensions.cpp
  • apps/openmw/mwstate/statemanagerimp.cpp
  • apps/openmw/mwworld/actionteleport.cpp
  • apps/openmw/mwworld/livecellref.cpp
  • apps/openmw/mwworld/scene.cpp
  • apps/openmw/mwworld/worldimp.cpp
  • apps/openmw/mwworld/worldmodel.cpp
  • components/lua/configuration.cpp
  • components/lua/configuration.hpp

Comment on lines +157 to +158
Misc::NotNullPtr<LuaManager> getLuaManager() const { return mLuaManager ? &mLuaManager->get() : nullptr; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

getLuaManager() violates its non-null return contract.

getLuaManager() can return nullptr even though its return type is Misc::NotNullPtr<LuaManager>, which can trigger assertions/crashes in callers expecting a guaranteed value. Align this accessor with getAuthoritativeLuaManager()/getClientLuaManager() and throw when unset (or make the return type nullable).

Suggested fix
-        Misc::NotNullPtr<LuaManager> getLuaManager() const { return mLuaManager ? &mLuaManager->get() : nullptr; }
+        Misc::NotNullPtr<LuaManager> getLuaManager() const
+        {
+            if (!mLuaManager)
+                throw std::logic_error("Lua manager is not registered");
+            return &mLuaManager->get();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Misc::NotNullPtr<LuaManager> getLuaManager() const { return mLuaManager ? &mLuaManager->get() : nullptr; }
Misc::NotNullPtr<LuaManager> getLuaManager() const
{
if (!mLuaManager)
throw std::logic_error("Lua manager is not registered");
return &mLuaManager->get();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/openmw/mwbase/environment.hpp` around lines 157 - 158, The
getLuaManager() method has a return type of Misc::NotNullPtr<LuaManager> which
guarantees a non-null return value, but the current implementation can return
nullptr when mLuaManager is null or falsy in the ternary expression. Fix this by
throwing an exception when mLuaManager is unset, similar to how
getAuthoritativeLuaManager() and getClientLuaManager() are implemented. Replace
the ternary operator with a check that throws when the value is not set,
ensuring the method's non-null contract is upheld.

Comment on lines +23 to +25
explicit Visitor(GlobalScripts& globalScripts, bool localScriptsEnabled)
: mGlobalScripts(globalScripts)
, mLocalScriptsEnabled(localScriptsEnabled)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Route player-active global handlers by Lua authority, not dedicated-server mode.

The visitor only receives the local-script flag, so OnActive still uses NetworkManager::isDedicatedServer() for the player branch. In host mode the authoritative Lua manager owns GLOBAL scripts but is not a dedicated server, so mGlobalScripts.playerAdded(...) is skipped. Call the global handler for the authoritative/global runtime instead; client runtimes have no global scripts selected.

Suggested fix
             if (ptr.getCellRef().getRefId() == "player")
             {
-                const auto& netMan = MWBase::Environment::get().getNetworkManager();
-                if (!netMan->isDedicatedServer())
-                {
-                }
-                else
-                {
-                    mGlobalScripts.playerAdded(GObject(ptr));
-                }
+                mGlobalScripts.playerAdded(GObject(ptr));
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/openmw/mwlua/engineevents.cpp` around lines 23 - 25, The Visitor
constructor currently only receives the localScriptsEnabled flag, but the
OnActive handler relies on NetworkManager::isDedicatedServer() to route
player-active events to global scripts, which fails in host mode where the
authoritative Lua runtime is not a dedicated server. Modify the Visitor
constructor to accept an additional parameter indicating Lua authority (whether
this runtime is authoritative for global scripts), store it as a member variable
alongside mGlobalScripts and mLocalScriptsEnabled, and then update the OnActive
handler to use this authority flag instead of isDedicatedServer() when
determining whether to call mGlobalScripts.playerAdded(...).

Comment on lines +38 to +54
void LuaEvents::addLocalEvent(Local event)
{
if (!mLocalEventsEnabled)
return;
mNewLocalEventBatch.push_back(std::move(event));
}

void LuaEvents::callEventHandlers()
{
for (const Global& e : mGlobalEventBatch)
mGlobalScripts.receiveEvent(e.mEventName, e.mEventData);
mGlobalEventBatch.clear();
if (!mLocalEventsEnabled)
{
mLocalEventBatch.clear();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve client-owned local event batches across saves.

With this guard, local event batches now live only in the client Lua manager, but the provided save path serializes only getLuaManagerForGlobalScripts()->write(). Pending mLocalEventBatch / mNewLocalEventBatch entries sent just before saving are no longer written or restored. Please add client Lua event serialization/load handling, or intentionally migrate local queued events into the local-script save path.

🧰 Tools
🪛 Cppcheck (2.21.0)

[style] 39-39: The function 'onUseItem' is never used.

(unusedFunction)


[style] 43-43: The function 'onNewExterior' is never used.

(unusedFunction)


[style] 38-38: The function 'addLocalEvent' is never used.

(unusedFunction)


[style] 45-45: The function 'callEventHandlers' is never used.

(unusedFunction)


[style] 50-50: The function 'addGlobalEvent' is never used.

(unusedFunction)


[style] 51-51: The function 'addMenuEvent' is never used.

(unusedFunction)


[style] 38-38: The function 'stateChanged' is never used.

(unusedFunction)


[style] 40-40: The function 'consoleCommand' is never used.

(unusedFunction)


[style] 46-46: The function 'onViewportResized' is never used.

(unusedFunction)


[style] 48-48: The function 'uiModeChanged' is never used.

(unusedFunction)


[style] 51-51: The function 'isLObject' is never used.

(unusedFunction)


[style] 47-47: The function 'setVarByInt' is never used.

(unusedFunction)


[style] 50-50: The function 'getId' is never used.

(unusedFunction)


[style] 38-38: The function 'getGridX' is never used.

(unusedFunction)


[style] 39-39: The function 'getGridY' is never used.

(unusedFunction)


[style] 40-40: The function 'isExterior' is never used.

(unusedFunction)


[style] 41-41: The function 'isQuasiExterior' is never used.

(unusedFunction)


[style] 42-42: The function 'hasWater' is never used.

(unusedFunction)


[style] 43-43: The function 'noSleep' is never used.

(unusedFunction)


[style] 44-44: The function 'getRegion' is never used.

(unusedFunction)


[style] 45-45: The function 'getNameId' is never used.

(unusedFunction)


[style] 46-46: The function 'getDisplayName' is never used.

(unusedFunction)


[style] 47-47: The function 'getDescription' is never used.

(unusedFunction)


[style] 48-48: The function 'getMood' is never used.

(unusedFunction)


[style] 49-49: The function 'getWaterHeight' is never used.

(unusedFunction)


[style] 53-53: The function 'getExteriorCellLocation' is never used.

(unusedFunction)


[style] 39-39: The function 'unsetRefNum' is never used.

(unusedFunction)


[style] 47-47: The function 'getClass' is never used.

(unusedFunction)


[style] 46-46: The function 'remove' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/openmw/mwlua/luaevents.cpp` around lines 38 - 54, The local event
batches (mLocalEventBatch and mNewLocalEventBatch) in the LuaEvents class are
not being serialized when the game saves, causing pending local events to be
lost. You need to add serialization and deserialization handling for these local
event batches, similar to how global scripts are handled with
getLuaManagerForGlobalScripts()->write(). Add write() and read() methods (or
update existing save/load mechanisms) in the LuaEvents class to persist
mLocalEventBatch and mNewLocalEventBatch to the save file and restore them on
load, ensuring local events queued just before saving are preserved across game
sessions.

Comment on lines +376 to 380
if (isAuthoritativeServer())
{
applyDelayedActions();
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defer authoritative reloads instead of reloading inline.

reloadAllScripts() can be reached from Lua-facing reload paths while the authoritative Lua runtime is executing. Calling reloadAllScriptsImpl() immediately mutates script cache/containers reentrantly and also violates the “end of next synchronizedUpdate” contract. Set the flag for both runtimes and service it in the authoritative branch after delayed actions.

Suggested fix
     if (isAuthoritativeServer())
     {
         applyDelayedActions();
+        if (mReloadAllScriptsRequested)
+        {
+            reloadAllScriptsImpl();
+            mReloadAllScriptsRequested = false;
+        }
         return;
     }
 void LuaManager::reloadAllScripts()
 {
-    if (ownsClientScriptContexts())
-        mReloadAllScriptsRequested = true;
-    else
-        reloadAllScriptsImpl();
+    mReloadAllScriptsRequested = true;
 }

Also applies to: 924-929

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/openmw/mwlua/luamanagerimp.cpp` around lines 376 - 380, The
reloadAllScripts() function is calling reloadAllScriptsImpl() immediately, which
causes reentrancy issues when Lua-facing reload paths execute during the
authoritative Lua runtime. Instead of directly invoking reloadAllScriptsImpl(),
set a flag to defer the reload operation and service it later in the
authoritative server's control flow. Specifically, set the reload flag
unconditionally in reloadAllScripts() (for both runtimes), and then check and
execute the actual reload in the isAuthoritativeServer() branch after calling
applyDelayedActions() to ensure it happens at the safe point after
synchronizedUpdate completes. Apply this same pattern to the additional
locations mentioned at lines 924-929.

Comment on lines +418 to +421
objectT["hasScript"] = [](const GObject&, std::string_view) { return false; };
objectT["removeScript"] = [](const GObject&, std::string_view) {
// TODO: global-to-local script removal needs an explicit authority boundary.
throw std::runtime_error("Global object scripts do not own local script containers");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

hasScript returning false is misleading; consider throwing or returning a distinct indicator.

The asymmetry between hasScript (silently returns false) and removeScript (throws with explanation) could cause subtle bugs. A Lua script checking if object:hasScript("myScript") will get false even when the script exists, leading to incorrect control flow.

Consider either:

  1. Throwing with the same authority-boundary explanation as removeScript
  2. Returning nil to indicate "unknown/inaccessible" rather than definitively false
Option 1: Throw like removeScript
-objectT["hasScript"] = [](const GObject&, std::string_view) { return false; };
+objectT["hasScript"] = [](const GObject&, std::string_view) -> bool {
+    throw std::runtime_error("Global object scripts do not own local script containers");
+};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/openmw/mwlua/objectbindings.cpp` around lines 418 - 421, The hasScript
lambda function returns false unconditionally for global objects, but this is
inconsistent with the removeScript lambda which throws an exception explaining
the authority boundary issue. Update the hasScript lambda (at
objectT["hasScript"]) to throw the same std::runtime_error exception with an
appropriate message explaining that global object scripts cannot be accessed due
to authority boundary restrictions, rather than silently returning false, so
that Lua code receives clear feedback about why the operation cannot be
performed.

Comment on lines +17 to +23
if (!environment.hasAuthoritativeLuaManager())
{
Log(Debug::Error) << "LuaEventRouter: dropping networked global event '" << eventName << "' from client "
<< clientIndex
<< " because authoritative Lua manager is not registered yet (transitional Host state)";
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid lossy handling before authoritative runtime registration.

Line 17–23 drops valid networked global events during host bootstrap; those events are never replayed, so authoritative/client state can diverge. Buffer-and-flush (or explicitly NACK/retry) instead of dropping.

🧰 Tools
🪛 Cppcheck (2.21.0)

[style] 17-17: The function 'getId' is never used.

(unusedFunction)


[style] 18-18: The function 'OnServerClientDisconnected' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/openmw/mwnet/luaeventrouter.cpp` around lines 17 - 23, The issue is that
the LuaEventRouter is dropping valid networked global events during the
transitional Host state when the authoritative Lua manager has not yet been
registered (the early return in the environment.hasAuthoritativeLuaManager()
check). Instead of immediately returning and discarding the event, implement a
buffering mechanism to queue these events and flush them once the authoritative
Lua manager becomes available, or alternatively implement an explicit NACK or
retry mechanism to ensure events are redelivered. Modify the logic around the
environment.hasAuthoritativeLuaManager() check to cache incoming events in a
queue during the bootstrap phase, then process and clear that queue once the
authoritative manager registers.

@magicaldave

Copy link
Copy Markdown
Member Author

In-game smoke test notes

I did a quick in-game smoke test of the Lua authority split using my wacky portable Linux build system.

Startup / host-client path

The game now reaches playable state as Host. The in-process client connects to the authoritative server:

[23:15:24.299 I] SERVER: client connected: 0
client connected to server

So the basic host runtime + local client endpoint path is alive. Nice.

Confirmed working: client/menu -> authoritative GLOBAL events

I temporarily added a test eventHandler to one of the deployed built-in GLOBAL scripts in the portable build only, not in the repo:

DREAMWEAVE_TEST_GLOBAL_EVENT = function(firstArg)
    print('DREAMWEAVE_TEST_GLOBAL_EVENT firstArg=' .. tostring(firstArg))
end,

Then I sent events from both player and menu Lua console contexts:

core.sendGlobalEvent('DREAMWEAVE_TEST_GLOBAL_EVENT', 'hello from client')
core.sendGlobalEvent('DREAMWEAVE_TEST_GLOBAL_EVENT', 'hello from menu')

The authoritative GLOBAL script received them:

[23:15:44.085 I] Global[scripts/omw/settings/global.lua]:       DREAMWEAVE_TEST_GLOBAL_EVENT firstArg=hello from client
[23:15:54.185 I] Global[scripts/omw/settings/global.lua]:       DREAMWEAVE_TEST_GLOBAL_EVENT firstArg=hello from client
[23:16:01.723 I] Global[scripts/omw/settings/global.lua]:       DREAMWEAVE_TEST_GLOBAL_EVENT firstArg=hello from menu

This confirms the important path we actually cared about here:

PLAYER / MENU client Lua
  -> sendGlobalEvent
  -> LuaEventRouter / in-process host-client path
  -> authoritative GLOBAL Lua eventHandlers

Known broken: client access to global storage

Some built-in client/menu/local scripts still assume storage.globalSection(...) is directly readable from the client Lua runtime. That is no longer true after the split, because the client Lua manager owns active player storage but not active global storage. So this one is not surprising, but it is very useful to have it loudly identified instead of quietly wrong.

Observed errors:

[23:06:53.699 E] Can't start L@0x1[scripts/omw/combat/local.lua]; Lua error: Trying to access inactive storage

and:

[23:06:54.012 E] Menu[scripts/omw/settings/menu.lua] onStateChanged failed. Lua error: Trying to access inactive storage
[23:06:54.012 E] stack traceback:
[23:06:54.012 E]        [C]: in function 'getSection'
[23:06:54.012 E]        [string "scripts/omw/settings/menu.lua"]:424: in function 'updateGroups'
[23:06:54.012 E]        [string "scripts/omw/settings/menu.lua"]:439: in function 'updateGlobalGroups'
[23:06:54.012 E]        [string "scripts/omw/settings/menu.lua"]:529: in function <[string "scripts/omw/settings/menu.lua"]:526>

Root cause: client-side packages still expose storage.globalSection, but that binding points at the client Lua manager’s inactive mGlobalStorage. The guard is doing its job. What is missing is a proper read-only mirror/proxy of authoritative global storage for client contexts.

This should not be fixed by simply making client global storage active, because that would recreate fake client-owned global state. That would be lying, and we already have enough of that. The remaining work is explicit synchronization/mirroring.

Known broken / invalidated: luas selected-object console

luap and luam work well enough to send test events. luas appears broken, or at least architecturally invalid after the split.

The current built-in console path for luas is:

PLAYER console script
  -> core.sendGlobalEvent('OMWConsoleStartLocal', { player, selected })

GLOBAL console script
  -> selected:addScript('scripts/omw/console/local.lua')
  -> player:sendEvent('OMWConsoleSetContext', selected)

then:
PLAYER console script
  -> selected:sendEvent('OMWConsoleEval', ...)

That relies on a GLOBAL script attaching a LOCAL console script to the selected object. After authority segmentation, that global/local side-door is exactly the kind of coupling this branch exposes. luas should likely become a client/local console operation instead of routing through authoritative global Lua.

Summary

What works:

  • Host startup.
  • In-process client connection.
  • Player/menu Lua contexts sending global events.
  • Authoritative GLOBAL scripts receiving those events.

What is still broken/expected follow-up:

  • Client-side read-only access to authoritative global storage.
  • Built-in scripts that assume storage.globalSection(...) works locally.
  • luas selected-object console mode, because it relied on global scripts mutating local script attachment.

So this branch proves the event boundary is functioning, but it also exposes the remaining places where old single-runtime assumptions still exist. Which is annoying, but also the point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant