Skip to content

gaowanlu/mapsvr

Repository files navigation

MapSvr

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.

How to Build

Please refer to the Dockerfile in this project for the complete image build process.

Configuration Files

Configuration files are located under the config directory:

Task types can be configured as either TCP Stream or WebSocket.

How to Add a New Protocol

There are two important core concepts in MapSvr:

  • Protocol-based communication
  • Asynchronous processing

All inter-process communication is performed via asynchronous protocol messages.

Protocol Definition

  • All .proto files are placed under the protocol directory.
  • After adding a new protocol, it must be registered in lua_plugin.cpp by defining a new mapping between Cmd and the corresponding Protobuf message factory.

Once registered, when Avant receives a known protocol message, it will:

  1. Convert the C++ Protobuf message into a Lua table
  2. 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.

Example: Registering Protocol Messages

// 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);
}

Generating Lua Type Annotations from Protobuf

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");

Message Handling in Lua

All protocol handling logic is located in MsgHandlerLogic.lua.

  • MsgHandler:HandlerMsgFromUDP Handles incoming UDP packets
  • MsgHandler:HandlerMsgFromOther Handles messages from other server processes
  • MsgHandler:HandlerMsgFromClient Handles messages from client connections
  • MsgHandler:Send2UDP Sends UDP packets to a target IP and port
  • MsgHandler:Send2IPC Sends protocol messages to other processes
  • MsgHandler:Send2Client Sends protocol messages to client connections (WebSocket or TCP)

About dbsvrgo

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.1

How to Shut Down the Server Properly

Define 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

How to Hot-Reload Game Logic

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 PID

MapSvr.OnReload reloads the specified Lua logic files.

⚠️ Warning

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.

Debugging Lua Code

Recommended setup:

  • VSCode
  • EmmyLua (VSCode extension)

Building emmy_core.so

Reference: https://github.com/EmmyLua/EmmyLuaDebugger

mkdir build
cd build
cmake .. -DEMMY_LUA_VERSION=54 -DCMAKE_BUILD_TYPE=Release
cmake --build . --config Release

CMake Integration

Copy 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})

Using emmy_core in Lua

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()
end

VSCode launch.json

MapSvr/.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
        }
    ]
}

Starting Avant

After starting the Avant process, the other thread will block at Other_dbg.waitIDE(), waiting for the debugger to attach.

Connecting VSCode to Other_dbg

In VSCode, open Run and Debug, select EmmyLua New Debug, and start debugging. Once connected, execution will pause when Other_dbg.breakHere() is reached.

Lua Circular Dependency Issue

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

Why This Happens

Lua’s require() mechanism works as follows:

  1. When a module is required, Lua first creates an empty module entry and places it into package.loaded.
  2. Lua then executes the module file.
  3. 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.

How This Appears in Real Projects

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;

Key Takeaway

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.

About

Game Server Framework powered by @mfavant/avant.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published