Implement clientless transports, Lua reload and same-map portals
Add native transport recording and execution with grid lifecycle handling, clientless teleport acknowledgements, and regression tests. Support targeted same-map portal imports and preserve disabled taxi nodes. Document live transport and portal verification and the remaining portal inventory.
This commit is contained in:
1 parent
01a9ba4b0b
commit
1164a72ea3
40 files changed
+4533
-163
No files matched your search
@@ -0,0 +1,145 @@
|
||||
"""Compile the production reload/bindings with real Lua and deterministic server doubles."""
|
||||
import pathlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
SOURCE = (ROOT / "src/server/game/World/ClientlessLuaEngine.cpp").read_text()
|
||||
|
||||
|
||||
def function(signature):
|
||||
start = SOURCE.index(signature)
|
||||
opening = SOURCE.index("{", start)
|
||||
depth = 1
|
||||
end = opening + 1
|
||||
while depth:
|
||||
depth += (SOURCE[end] == "{") - (SOURCE[end] == "}")
|
||||
end += 1
|
||||
return SOURCE[start:end]
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#include <lua.hpp>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
using uint32 = uint32_t;
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
struct ClientlessIntent {
|
||||
std::string Name;
|
||||
std::unordered_map<std::string,std::string> Parameters;
|
||||
bool Empty() const { return Name.empty(); }
|
||||
};
|
||||
using ClientlessAction = ClientlessIntent;
|
||||
struct ClientlessAdventureProfile { int Revision = 0; };
|
||||
struct Guid { uint32 GetCounter() const { return 26; } };
|
||||
struct Player {
|
||||
bool aboard = false, flying = false, teleport = false;
|
||||
Guid GetGUID() { return {}; }
|
||||
bool IsInWorld() { return true; }
|
||||
bool GetTransport() { return aboard; }
|
||||
bool IsInFlight() { return flying; }
|
||||
bool IsBeingTeleported() { return teleport; }
|
||||
};
|
||||
enum class ClientlessActionExecutionState { Idle, SelectingTarget, Moving, Completed, Failed };
|
||||
enum class ClientlessTravelState { Idle, Moving, Arrived };
|
||||
struct ActionStatus { ClientlessActionExecutionState State = ClientlessActionExecutionState::Idle; };
|
||||
struct TravelStatus { ClientlessTravelState State = ClientlessTravelState::Idle; };
|
||||
struct Manager {
|
||||
bool managed = true;
|
||||
std::vector<ClientlessAction> actions;
|
||||
ClientlessIntent intent;
|
||||
bool IsManaged(uint32) { return managed; }
|
||||
unsigned GetQueuedActionCount(uint32) { return actions.size(); }
|
||||
bool QueueAction(uint32, ClientlessAction a) { actions.push_back(std::move(a)); return true; }
|
||||
bool SetIntent(uint32, ClientlessIntent i) { intent = std::move(i); return true; }
|
||||
bool ClearIntent(uint32) { intent = {}; return true; }
|
||||
} manager;
|
||||
struct Executor { ActionStatus status; ActionStatus* GetStatus(uint32) { return &status; } } executor;
|
||||
struct Travel { TravelStatus status; TravelStatus* GetStatus(uint32) { return &status; } } travel;
|
||||
auto* sClientlessPlayerMgr = &manager;
|
||||
auto* sClientlessActionExecutorMgr = &executor;
|
||||
auto* sClientlessTravelMgr = &travel;
|
||||
class ClientlessLuaEngine {
|
||||
public:
|
||||
std::unordered_map<uint32,ClientlessAdventureProfile> _profiles;
|
||||
std::string _scriptRoot = "/test";
|
||||
std::string script;
|
||||
bool profileOK = true;
|
||||
bool LoadAdventureProfile(Player*) {
|
||||
if (!profileOK) return false;
|
||||
_profiles[26].Revision++;
|
||||
return true;
|
||||
}
|
||||
bool ExecuteAdventureScript(Player*);
|
||||
bool ReloadAdventureScript(Player*, std::string&);
|
||||
};
|
||||
'''
|
||||
HARNESS += SOURCE[SOURCE.index("struct ReloadEffects"):SOURCE.index("uint32 BoundGuid")]
|
||||
for signature in ["uint32 BoundGuid", "std::unordered_map<std::string, std::string> ReadMap",
|
||||
"int LuaIntend", "int LuaClearIntent", "int LuaQueueAction"]:
|
||||
HARNESS += function(signature) + "\n"
|
||||
HARNESS += r'''
|
||||
bool ClientlessLuaEngine::ExecuteAdventureScript(Player*) {
|
||||
lua_State* L = luaL_newstate();
|
||||
luaL_openlibs(L);
|
||||
for (auto entry : {std::pair{"intend", LuaIntend}, {"clear_intent", LuaClearIntent}, {"queue_action", LuaQueueAction}}) {
|
||||
lua_pushinteger(L,26); lua_pushcclosure(L,entry.second,1); lua_setglobal(L,entry.first);
|
||||
}
|
||||
bool success = luaL_loadstring(L, script.c_str()) == LUA_OK && lua_pcall(L,0,0,0) == LUA_OK;
|
||||
lua_close(L);
|
||||
return success;
|
||||
}
|
||||
'''
|
||||
HARNESS += function("bool ClientlessLuaEngine::ReloadAdventureScript")
|
||||
HARNESS += r'''
|
||||
int main() {
|
||||
Player p;
|
||||
ClientlessLuaEngine engine;
|
||||
std::string detail;
|
||||
engine._profiles[26].Revision = 4;
|
||||
manager.intent.Name = "old";
|
||||
engine.script = "queue_action('move', {node='tram_if'}); intend('new'); error('failure')";
|
||||
assert(!engine.ReloadAdventureScript(&p,detail));
|
||||
assert(manager.actions.empty() && manager.intent.Name == "old");
|
||||
assert(engine._profiles.at(26).Revision == 4 && !pendingReload);
|
||||
engine.script = "this is invalid lua";
|
||||
assert(!engine.ReloadAdventureScript(&p,detail));
|
||||
assert(engine._profiles.at(26).Revision == 4 && manager.actions.empty());
|
||||
engine.profileOK = false;
|
||||
assert(!engine.ReloadAdventureScript(&p,detail));
|
||||
engine.profileOK = true;
|
||||
engine.script = "assert(not queue_action('')); queue_action('move', {node='tram_if'}); clear_intent(); intend('new')";
|
||||
assert(engine.ReloadAdventureScript(&p,detail));
|
||||
assert(manager.actions.size() == 1 && manager.actions[0].Parameters.at("node") == "tram_if");
|
||||
assert(manager.intent.Name == "new" && engine._profiles.at(26).Revision == 5);
|
||||
assert(!engine.ReloadAdventureScript(&p,detail)); // queued actions must not duplicate
|
||||
manager.actions.clear();
|
||||
executor.status.State = ClientlessActionExecutionState::Moving;
|
||||
assert(!engine.ReloadAdventureScript(&p,detail));
|
||||
executor.status.State = ClientlessActionExecutionState::Completed;
|
||||
travel.status.State = ClientlessTravelState::Moving;
|
||||
assert(!engine.ReloadAdventureScript(&p,detail));
|
||||
travel.status.State = ClientlessTravelState::Arrived;
|
||||
p.aboard = true; assert(!engine.ReloadAdventureScript(&p,detail)); p.aboard = false;
|
||||
p.flying = true; assert(!engine.ReloadAdventureScript(&p,detail)); p.flying = false;
|
||||
p.teleport = true; assert(!engine.ReloadAdventureScript(&p,detail)); p.teleport = false;
|
||||
assert(!engine.ReloadAdventureScript(nullptr,detail));
|
||||
manager.managed = false; assert(!engine.ReloadAdventureScript(&p,detail)); manager.managed = true;
|
||||
engine._profiles.clear(); engine.script = "error('failure')";
|
||||
assert(!engine.ReloadAdventureScript(&p,detail) && engine._profiles.empty());
|
||||
engine.script = "clear_intent()";
|
||||
assert(engine.ReloadAdventureScript(&p,detail) && manager.intent.Empty());
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="vcore-lua-reload-") as folder:
|
||||
source = pathlib.Path(folder) / "test.cpp"
|
||||
binary = pathlib.Path(folder) / "test"
|
||||
source.write_text(HARNESS)
|
||||
flags = subprocess.check_output(["pkg-config", "--cflags", "--libs", "lua5.4"], text=True).split()
|
||||
subprocess.run(["c++", "-std=c++17", str(source), "-o", str(binary), *flags], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
print("Lua reload regression tests passed (production reload/bindings, real Lua 5.4).")
|
||||
Reference in new issue
Block a user