diff --git a/CMakeLists.txt b/CMakeLists.txt index cef0d74..19aa496 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ add_executable(openfusion ${SOURCES}) set_target_properties(openfusion PROPERTIES OUTPUT_NAME ${BIN_NAME}) target_link_libraries(openfusion sqlite3) +target_link_libraries(openfusion lua51) # Makes it so config, tdata, etc. get picked up when starting via the debugger in VS set_property(TARGET openfusion PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/Makefile b/Makefile index fa2f70a..b91b0a3 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ CXX=clang++ # If compiling with ASAN, invoke like this: $ LSAN_OPTIONS=suppressions=suppr.txt bin/fusion CFLAGS=-O3 #-g3 -fsanitize=address CXXFLAGS=-Wall -Wno-unknown-pragmas -std=c++17 -O2 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor #-g3 -fsanitize=address -LDFLAGS=-lpthread -lsqlite3 #-g3 -fsanitize=address +LDFLAGS=-lpthread -lsqlite3 -lluajit-5.1 -lstdc++fs #-g3 -fsanitize=address # specifies the name of our exectuable SERVER=bin/fusion @@ -19,7 +19,7 @@ WIN_CC=x86_64-w64-mingw32-gcc WIN_CXX=x86_64-w64-mingw32-g++ WIN_CFLAGS=-O3 #-g3 -fsanitize=address WIN_CXXFLAGS=-D_WIN32_WINNT=0x0601 -Wall -Wno-unknown-pragmas -std=c++17 -O3 -DPROTOCOL_VERSION=$(PROTOCOL_VERSION) -DGIT_VERSION=\"$(GIT_VERSION)\" -I./src -I./vendor #-g3 -fsanitize=address -WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3 #-g3 -fsanitize=address +WIN_LDFLAGS=-static -lws2_32 -lwsock32 -lsqlite3 -lluajit-5.1 -lstdc++fs #-g3 -fsanitize=address WIN_SERVER=bin/winfusion.exe # C code; currently exclusively from vendored libraries @@ -43,6 +43,7 @@ CXXSRC=\ src/servers/CNLoginServer.cpp\ src/servers/CNShardServer.cpp\ src/servers/Monitor.cpp\ + src/lua/LuaManager.cpp\ src/db/init.cpp\ src/db/login.cpp\ src/db/shard.cpp\ @@ -86,6 +87,7 @@ CXXHDR=\ src/servers/CNLoginServer.hpp\ src/servers/CNShardServer.hpp\ src/servers/Monitor.hpp\ + src/lua/LuaManager.hpp\ src/db/Database.hpp\ src/db/internal.hpp\ vendor/bcrypt/BCrypt.hpp\ diff --git a/appveyor.yml b/appveyor.yml index fe39d2f..caa0f98 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,6 +18,8 @@ for: matrix: only: - image: GCP-Linux-Ubuntu2004 + install: + - sh: sudo apt install libluajit-5.1-dev -y build_script: - ps: | $versions = "104", "728", "1013" @@ -48,6 +50,7 @@ for: - image: GCP-Windows-VS2019 install: - cmd: vcpkg install sqlite3:x64-windows + - cmd: vcpkg install luajit:x64-windows - cmd: vcpkg integrate install build_script: - ps: | diff --git a/scripts/test.lua b/scripts/test.lua new file mode 100644 index 0000000..0371c5c --- /dev/null +++ b/scripts/test.lua @@ -0,0 +1,3 @@ +print("Hello world!") +wait(2) +print("Hello world ~2 seconds later!") \ No newline at end of file diff --git a/src/lua/LuaManager.cpp b/src/lua/LuaManager.cpp new file mode 100644 index 0000000..da16f3b --- /dev/null +++ b/src/lua/LuaManager.cpp @@ -0,0 +1,156 @@ +#include "lua/LuaManager.hpp" + +#include "servers/CNShardServer.hpp" +#include "settings.hpp" + +#include +#include + +time_t getTime(); +class Script; + +// our "main" state, holds our environment +lua_State *LuaManager::global; +std::map activeScripts; + +/* + Basically each script is treated as a coroutine, when wait() is called it gets yielded and is pushed onto the scheduler queue to be resumed. +*/ +class Script { +private: + lua_State *thread; + lRegistry threadRef; // we'll need to unref this when closing this state + +public: + Script(std::string source) { + // make the thread & register it in the registry + thread = lua_newthread(LuaManager::global); + threadRef = luaL_ref(LuaManager::global, LUA_REGISTRYINDEX); + + // add this script to the map + activeScripts[thread] = this; + + // compile & run the script, if it error'd, print the error + int _retCode; + if (luaL_loadfile(thread, source.c_str()) || ((_retCode = lua_resume(thread, 0)) != 0 && (_retCode != LUA_YIELD))) { + std::cout << "[LUA ERROR]: " << lua_tostring(thread, -1) << std::endl; + } + } + + // unregister all of our events from the wrappers + ~Script() { + LuaManager::clearState(thread); + + // remove it from the global registry + luaL_unref(LuaManager::global, LUA_REGISTRYINDEX, threadRef); + } + + // c++ moment.... + lua_State* getState() { + return thread; + } +}; + +struct scheduledThread { + lRegistry ref; // ref that should be unref'd before resuming + time_t time; +}; +std::map scheduleQueue; + +// pauses the script for x seconds +int OF_wait(lua_State *state) { + double seconds = luaL_checknumber(state, 1); + + // register the thread in the global registry so we don't get GC'd + lua_pushthread(state); + lRegistry ref = luaL_ref(state, LUA_REGISTRYINDEX); // threads decentant of a global state all share the global registry + + // yield the state and push the state onto our scheduler queue + scheduleQueue[state] = {ref, (int)(seconds*1000) + getTime()}; + return lua_yield(state, 0); +} + +void luaScheduler(CNServer *serv, time_t currtime) { + for (auto iter = scheduleQueue.begin(); iter != scheduleQueue.end();) { + time_t event = (*iter).second.time; + lRegistry ref = (*iter).second.ref; + lua_State *thread = (*iter).first; + // is it time to run the event? + if (event <= currtime) { + // remove from the scheduler queue + scheduleQueue.erase(iter++); + + // unregister the thread + luaL_unref(thread, LUA_REGISTRYINDEX, ref); + + // resume the state, (wait() returns the delta time since call) + lua_pushnumber(thread, ((double)currtime - event)/10); + + int err = lua_resume(thread, 1); + if (err != 0 && err != LUA_YIELD) // if it returned LUA_YIELD, wait() was just called again, not an error! + LuaManager::printError(lua_tostring(thread, -1)); + } else // go to the next iteration + ++iter; + } +} + +void LuaManager::printError(std::string err) { + std::cerr << "[LUA ERR]: " << err << std::endl; +} + +void LuaManager::init() { + // allocate our state + global = luaL_newstate(); + + // open lua's base libraries (excluding the IO for now) + luaopen_base(global); + luaopen_table(global); + luaopen_string(global); + luaopen_math(global); + luaopen_debug(global); + + // add wait() + lua_register(global, "wait", OF_wait); + + activeScripts = std::map(); + + REGISTER_SHARD_TIMER(luaScheduler, 200); + + // load our scripts + loadScripts(); +} + +void LuaManager::runScript(std::string filename) { + new Script(filename); +} + +void LuaManager::stopScripts() { + // clear the scheduler queue + scheduleQueue.clear(); + + // free all the scripts, they'll take care of everything for us :) + for (auto as : activeScripts) { + delete as.second; + } + + // finally clear the map + activeScripts.clear(); +} + +void LuaManager::loadScripts() { + if (!std::filesystem::exists(settings::SCRIPTSDIR)) { + std::cout << "[WARN] scripts directory \"" << settings::SCRIPTSDIR << "\" doesn't exist!" << std::endl; + return; + } + + // for each file in the scripts director, load the script + std::filesystem::path dir(settings::SCRIPTSDIR); + for (auto &d : std::filesystem::directory_iterator(dir)) { + if (d.path().extension().u8string() == ".lua") + runScript(d.path().u8string()); + } +} + +void LuaManager::clearState(lua_State *state) { + // TODO +} \ No newline at end of file diff --git a/src/lua/LuaManager.hpp b/src/lua/LuaManager.hpp new file mode 100644 index 0000000..9d09646 --- /dev/null +++ b/src/lua/LuaManager.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "core/CNProtocol.hpp" + +#include +#ifdef _WIN32 + #include +#else + #include +#endif + +typedef int lRegistry; + +namespace LuaManager { + extern lua_State *global; + void init(); + void printError(std::string err); + + // runs the script in the passed file + void runScript(std::string filename); + void stopScripts(); + void loadScripts(); + + // unregisters the events tied to this state with all wrappers + void clearState(lua_State *state); +} diff --git a/src/main.cpp b/src/main.cpp index c4ccb10..0f02324 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "servers/CNLoginServer.hpp" #include "servers/CNShardServer.hpp" +#include "lua/LuaManager.hpp" #include "PlayerManager.hpp" #include "PlayerMovement.hpp" #include "BuiltinCommands.hpp" @@ -120,6 +121,7 @@ int main() { Racing::init(); Database::open(); Trading::init(); + LuaManager::init(); switch (settings::EVENTMODE) { case 0: break; // no event diff --git a/src/settings.cpp b/src/settings.cpp index 8054e65..774f598 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -35,6 +35,7 @@ int settings::SPAWN_ANGLE = 130; std::string settings::DBPATH = "database.db"; std::string settings::TDATADIR = "tdata/"; std::string settings::PATCHDIR = "tdata/patch/"; +std::string settings::SCRIPTSDIR = "scripts"; std::string settings::NPCJSON = "NPCs.json"; std::string settings::MOBJSON = "mobs.json"; diff --git a/src/settings.hpp b/src/settings.hpp index d925fd4..fe17b57 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -27,6 +27,7 @@ namespace settings { extern std::string PATCHDIR; extern std::string ENABLEDPATCHES; extern std::string TDATADIR; + extern std::string SCRIPTSDIR; extern int EVENTMODE; extern bool MONITORENABLED; extern int MONITORPORT;