MapSvr is a game server framework built on top of @mfavant/avant.
It supports seamless hot-reloading of game logic without server downtime and allows clients to connect via TCP, UDP, and WebSocket. Inter-process communication between server instances is handled over TCP using Protocol Buffers.
See the Dockerfile for the complete build process.
Configuration files are located under the config directory:
Supported task types are TCP Stream and WebSocket.
MapSvr is built around two core concepts:
- Protocol-based communication
- Asynchronous processing
All inter-process communication uses asynchronous protocol messages.
- All
.protofiles live under theprotocol/directory. - After adding a new protocol, register it in
lua_plugin.cppby mapping theCmdvalue to 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 appropriate Lua VM for processing
The reverse conversion also applies: when Lua sends a Lua table to C++, it is 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);
}MapSvr relies heavily on Protobuf-defined types, so we auto-generate Lua type annotations
(including enums) from the .proto files.
The generate_proto_lua.js script reads all .proto files under the
protocol/ directory and generates corresponding Lua files into
ProtoLua/.
These generated files should be required in Lua code (e.g. in
MsgHandlerLogic.lua).
With the EmmyLua VSCode extension, this enables:
- Field auto-completion
- Type checking
- Enum hints
Example in MsgHandlerLogic.lua
local ProtoLuaCmd = require("ProtoLuaCmd");
local ProtoLuaDatabase = require("ProtoLuaDatabase");
local ProtoLuaExample = require("ProtoLuaExample");
local ProtoLuaIpcStream = require("ProtoLuaIpcStream");
local ProtoLuaLua = require("ProtoLuaLua");
local ProtoLuaMessageHead = require("ProtoLuaMessageHead");
local ProtoLuaTunnel = require("ProtoLuaTunnel");All protocol handling logic lives in MsgHandlerLogic.lua.
| Method | Description |
|---|---|
MsgHandler:HandlerMsgFromUDP |
Handles incoming UDP packets |
MsgHandler:HandlerMsgFromOther |
Handles messages from other server processes |
MsgHandler:HandlerMsgFromClient |
Handles messages from client connections |
MsgHandler:Send2UDP |
Sends a UDP packet to a target IP and port |
MsgHandler:Send2IPC |
Sends a protocol message to another process |
MsgHandler:Send2Client |
Sends a protocol message to a client (TCP or WebSocket) |
dbsvrgo is a Go-based database service that communicates with Avant over TCP using
Protocol Buffers. All database operations are handled exclusively inside dbsvrgo.
The Lua game logic in Avant communicates with dbsvrgo asynchronously via protocol messages:
avant (MapSvr luaVM) <---- TCP Protobuf ----> dbsvrgo (MySQL)
appId: 1.1.1.1 appId: 1.1.2.1
Define a custom UDP shutdown protocol and invoke
MapSvr.OnSafeStop() using the messages defined in
proto_udp.proto (ProtoUDPSafeStopReq / ProtoUDPSafeStopRes).
An example TypeScript UDP client is available at testing_client.ts.
Sending this shutdown message triggers cleanup logic before the process exits, such as:
- Kicking all connected players offline
- Persisting all player data to the database
- Rejecting new login attempts
Game logic can be hot-reloaded by sending a signal to the process — no server restart is needed.
Upon receiving the signal, MapSvr.OnReload is invoked:
kill -SIGUSR1 <PID>MapSvr.OnReload reloads the specified Lua logic files.
⚠️ Warning If an error occurs during reload (e.g. a syntax or runtime error), the process will crash immediately. This is a dangerous operation and should be used with caution. A crash during reload may interrupt in-flight database persistence logic, potentially causing data loss or rollback.
The recommended setup uses VSCode with the EmmyLua extension.
See the EmmyLuaDebugger repository for details.
mkdir build && cd build
cmake .. -DEMMY_LUA_VERSION=54 -DCMAKE_BUILD_TYPE=Release
cmake --build . --config ReleaseCopy the built emmy_core.so into the mapsvr directory and update lua_plugin.cpp as shown below.
// 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})local Other = {};
local Log = require("Log");
local MapSvr = require("MapSvr")
Other_dbg = {}; -- global dbg object
function Other:OnInit()
Other_dbg = require("emmy_core")
Other_dbg.tcpListen("127.0.0.1", 9966)
Other_dbg.waitIDE() -- wait for IDE connection
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() -- set breakpoint
MapSvr.OnTick()
endCreate .vscode/launch.json in the mapsvr directory:
{
"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
}
]
}Once the Avant process starts, the other thread blocks at Other_dbg.waitIDE(), waiting for
the debugger to connect.
In VSCode, open Run and Debug, select EmmyLua New Debug, and press Start.
Once connected, execution will pause at any Other_dbg.breakHere() calls.
See FAQ.md for frequently asked questions.