MapSvr is a game server framework built on top of @mfavant/avant.
It supports seamless logic hot-reloading without server downtime and allows clients to connect via TCP, UDP, and WebSocket. Inter-process communication between server instances is handled through TCP-based protocol exchange. All protocols are uniformly defined using @protocolbuffers/protobuf.
Please refer to the Dockerfile in this project for the complete image build process.
Configuration files are located under the config directory:
Task types can be configured as either TCP Stream or WebSocket.
There are two important core concepts in MapSvr:
- Protocol-based communication
- Asynchronous processing
All inter-process communication is performed via asynchronous protocol messages.
- All
.protofiles are placed under theprotocoldirectory. - After adding a new protocol, it must be registered in
lua_plugin.cppby defining a new mapping betweenCmdand the corresponding Protobuf message factory.
Once registered, when Avant receives a known protocol message, it will:
- Convert the C++ Protobuf message into a Lua table
- Dispatch it to the corresponding Lua VM for processing
Similarly, when Lua sends a Lua table to C++, it will be converted back into a C++ Protobuf message.
// Register protocols that need to interact between C++ and Lua
void lua_plugin::init_message_factory()
{
REGISTER_MSG(ProtoCmd::PROTO_CMD_LUA_TEST, ProtoLuaTest);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_REQ_EXAMPLE, ProtoCSReqExample);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_RES_EXAMPLE, ProtoCSResExample);
REGISTER_MSG(ProtoCmd::PROTO_CMD_TUNNEL_WORKER2OTHER_EVENT_NEW_CLIENT_CONNECTION, ProtoTunnelWorker2OtherEventNewClientConnection);
REGISTER_MSG(ProtoCmd::PROTO_CMD_TUNNEL_WORKER2OTHER_EVENT_CLOSE_CLIENT_CONNECTION, ProtoTunnelWorker2OtherEventCloseClientConnection);
REGISTER_MSG(ProtoCmd::PROTO_CMD_TUNNEL_OTHERLUAVM2WORKER_CLOSE_CLIENT_CONNECTION, ProtoTunnelOtherLuaVM2WorkerCloseClientConnection);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_REQ_LOGIN, ProtoCSReqLogin);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_RES_LOGIN, ProtoCSResLogin);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_MAP_NOTIFY_INIT_DATA, ProtoCSMapNotifyInitData);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_REQ_MAP_PING, ProtoCSReqMapPing);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_RES_MAP_PONG, ProtoCSResMapPong);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_REQ_MAP_INPUT, ProtoCSReqMapInput);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_MAP_NOTIFY_STATE_DATA, ProtoCSMapNotifyStateData);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_MAP_ENTER_REQ, ProtoCSMapEnterReq);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_MAP_ENTER_RES, ProtoCSMapEnterRes);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_MAP_LEAVE_REQ, ProtoCSMapLeaveReq);
REGISTER_MSG(ProtoCmd::PROTO_CMD_CS_MAP_LEAVE_RES, ProtoCSMapLeaveRes);
}We rely heavily on Protobuf-defined types and expect them to automatically generate Lua type annotations, including enum support.
The generate_lua.js script can generate corresponding Lua files for all .proto files under the protocol directory and place them into ProtoLua.
These files should then be required in Lua code (e.g. MsgHandlerLogic.lua).
With the EmmyLua plugin, this enables:
- Field auto-completion
- Type checking
- Enum hints
Example in MsgHandlerLogic.lua
local proto_cmd = require("proto_cmd");
local proto_database = require("proto_database");
local proto_example = require("proto_example");
local proto_ipc_stream = require("proto_ipc_stream");
local proto_lua = require("proto_lua");
local proto_message_head = require("proto_message_head");
local proto_tunnel = require("proto_tunnel");All protocol handling logic is located in MsgHandlerLogic.lua.
MsgHandler:HandlerMsgFromUDPHandles incoming UDP packetsMsgHandler:HandlerMsgFromOtherHandles messages from other server processesMsgHandler:HandlerMsgFromClientHandles messages from client connectionsMsgHandler:Send2UDPSends UDP packets to a target IP and portMsgHandler:Send2IPCSends protocol messages to other processesMsgHandler:Send2ClientSends protocol messages to client connections (WebSocket or TCP)
dbsvrgo is a database service written in Go that communicates with Avant via TCP Protobuf.
All database operations are handled exclusively within the dbsvrgo process.
Lua-based game logic servers communicate with dbsvrgo asynchronously using protocol messages.
avant(MapSvrGo luaVM) <---- TCP Protobuf ----> dbsvrgo(MySQL)
appId: 1.1.1.1 appId: 1.1.2.1Define your own UDP shutdown protocol.
Before stopping the process, send a custom UDP shutdown message to handle necessary logic such as:
- Forcing all players offline
- Persisting all player data to the database
- Preventing new player logins
Logic hot-reloading is triggered via a process signal without stopping the server.
When the signal is received, MapSvr.OnReload will be invoked.
kill -10 PIDMapSvr.OnReload reloads the specified Lua logic files.
If an error occurs during reload (e.g. syntax or runtime error), the process will crash immediately. This is a dangerous operation and should be avoided unless absolutely necessary.
A crash during reload may interrupt database persistence logic, potentially causing data loss or rollback.
Recommended setup:
- VSCode
- EmmyLua (VSCode extension)
Reference: https://github.com/EmmyLua/EmmyLuaDebugger
mkdir build
cd build
cmake .. -DEMMY_LUA_VERSION=54 -DCMAKE_BUILD_TYPE=Release
cmake --build . --config ReleaseCopy emmy_core.so into the MapSvr directory and update lua_plugin.cpp.
// Declare in lua_plugin.cpp
extern "C" int luaopen_emmy_core(lua_State *L);
// Load emmy_core into other_lua_state
luaL_requiref(this->other_lua_state, "emmy_core", luaopen_emmy_core, 1);
lua_pop(this->other_lua_state, 1);
void lua_plugin::on_other_init(avant::workers::other *ptr_other_obj)
{
this->ptr_other_obj = ptr_other_obj;
this->other_lua_state = luaL_newstate();
luaL_openlibs(this->other_lua_state);
luaL_requiref(this->other_lua_state, "emmy_core", luaopen_emmy_core, 1);
lua_pop(this->other_lua_state, 1);
other_mount();
std::string filename = this->lua_dir + "/Init.lua";
int isok = luaL_dofile(this->other_lua_state, filename.data());
lua_plugin::lua_plugin_lua_return_not_is_ok_print_error(isok, this->other_lua_state);
ASSERT_LOG_EXIT(isok == LUA_OK);
}Link emmy_core.so when building Avant:
target_link_libraries(${PROJECT_NAME} ... /path/to/emmy_core.so ${EXTERNAL_LIB})Other.lua
local Other = {};
local Log = require("Log");
local MapSvr = require("MapSvr")
Other_dbg = {}; -- creating global dbg object
function Other:OnInit()
Other_dbg = require("emmy_core")
Other_dbg.tcpListen("127.0.0.1", 9966)
Other_dbg.waitIDE() -- waiting for IDE
local log = "OnOtherInit";
Log:Error(log);
MapSvr.OnInit()
Other:OnReload();
end
function Other:OnStop()
local log = "OnOtherStop";
Log:Error(log);
MapSvr.OnStop()
end
function Other:OnTick()
Other_dbg.breakHere() -- setting break point
MapSvr.OnTick()
endMapSvr/.vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "emmylua_new",
"request": "launch",
"name": "EmmyLua New Debug",
"host": "127.0.0.1",
"port": 9966,
"ext": [
".lua",
".lua.txt",
".lua.bytes"
],
"ideConnectDebugger": true
}
]
}After starting the Avant process, the other thread will block at
Other_dbg.waitIDE(), waiting for the debugger to attach.
In VSCode, open Run and Debug, select EmmyLua New Debug, and start debugging.
Once connected, execution will pause when Other_dbg.breakHere() is reached.
For example:
main.lua
print("main.lua");
local AFunc = require("./A");
AFunc();
A.lua
print("A.lua");
local BFunc = require("./B");
function AFunc()
print("A");
BFunc();
end
return AFunc;B.lua
print("B.lua");
local CFunc = require("./C");
function BFunc()
print("B");
CFunc();
end
return BFunc;C.lua
print("C.lua");
local AFunc = require("./A");
function CFunc()
print("C");
AFunc();
end
return CFunc;Running the program:
$ lua main.lua
A.lua B.lua C.lua A.lua B.lua C.lua A.lua B.lua C.lua A.lua B.lua C.lua A.lua B.lua lua: error loading module './C' from file './//C.lua': C stack overflow stack traceback: [C]: in ? [C]: in function 'require' .///B.lua:2: in main chunk [C]: in function 'require' .///A.lua:2: in main chunk [C]: in function 'require' .///C.lua:2: in main chunk [C]: in function 'require' .///B.lua:2: in main chunk [C]: in function 'require' ... (skipping 370 levels) [C]: in function 'require' .///A.lua:2: in main chunk [C]: in function 'require' .///C.lua:2: in main chunk [C]: in function 'require' .///B.lua:2: in main chunk [C]: in function 'require' .///A.lua:2: in main chunk [C]: in function 'require' main.lua:2: in main chunk [C]: in ? stack traceback: [C]: in ? [C]: in function 'require' .///B.lua:2: in main chunk [C]: in function 'require' .///A.lua:2: in main chunk [C]: in function 'require' .///C.lua:2: in main chunk [C]: in function 'require' .///B.lua:2: in main chunk [C]: in function 'require' ... (skipping 370 levels) [C]: in function 'require' .///A.lua:2: in main chunk [C]: in function 'require' .///C.lua:2: in main chunk [C]: in function 'require' .///B.lua:2: in main chunk [C]: in function 'require' .///A.lua:2: in main chunk [C]: in function 'require' main.lua:2: in main chunk [C]: in ?The three files above form a perfect circular require chain:
- A.lua require B.lua
- B.lua require C.lua
- C.lua require A.lua
Lua’s require() mechanism works as follows:
- When a module is required, Lua first creates an empty module entry and places it into
package.loaded. - Lua then executes the module file.
- If, during execution, the module requires another module that eventually leads back to itself, Lua will recursively execute module chunks again.
Because each module has not yet returned when the next require() happens, the corresponding entry in package.loaded remains in a "loading..." state.
When C.lua calls require("./A") again, Lua sees that A exists in package.loaded, but since the module is still loading, Lua does not stop execution. Instead, it executes A.lua’s chunk again, leading to infinite recursion and eventually a C stack overflow.
In real-world business code, especially in complex systems where modules depend on each other, this kind of circular require issue is common.
Don’t panic — check the logs, identify the dependency cycle, and refactor slightly to break it.
For example, modifying C.lua like this:
print("C.lua");
function CFunc()
local AFunc = require("./A");
print("C");
AFunc();
end
return CFunc;When loading a module, Lua checks package.loaded[name]:
- If the value exists and is not nil, Lua returns it directly.
- If the value is
true(indicating the module is currently loading), Lua does not return immediately. - Instead, Lua continues executing the loader’s returned chunk (i.e., the module file).
In other words, true in package.loaded is only a loading marker, not a guard against re-executing the module file. This is why circular require chains can still cause infinite recursion.