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.
146 lines
6.5 KiB
Python
146 lines
6.5 KiB
Python
"""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).")
|