Skip to content

服务端模组 (GameModuleServer)

服务端模组是 Module 开发的核心。它继承 GameModuleServer,负责创建 GamingQueue、处理客户端消息、驱动游戏循环。

基本模板

csharp
public class ExampleGameModuleServer : GameModuleServer, IHotReloadAware
{
    public override string Name => ExampleGameModuleConstant.ExampleGameModule;
    public override string Description => "My First GameModule";
    public override string Version => "1.0.0";
    public override string Author => "FunGamer";

    // 工作类:隔离每个房间的状态(模组是单例的!)
    private readonly struct ModuleServerWorker(GamingObject obj)
    {
        public GamingObject GamingObject { get; } = obj;
        public List<User> ConnectedUser { get; } = [];
        public List<Character> CharactersForPick { get; } = [];
        public Dictionary<string, Character> UserCharacters { get; } = [];
        public Dictionary<string, Dictionary<string, object>> UserData { get; } = [];
    }

    private ConcurrentDictionary<string, ModuleServerWorker> Workers { get; } = [];
}

重要

模组是单例的。多个房间共享同一个模组实例。必须用 ConcurrentDictionary<string, Worker> 来隔离每个房间的状态!


启动服务端

csharp
public override bool StartServer(GamingObject obj, params object[] args)
{
    // 为这个房间创建一个工作类
    ModuleServerWorker worker = new(obj);
    Workers[obj.Room.Roomid] = worker;

    // 创建线程执行游戏逻辑(此方法必须立即返回)
    TaskUtility.NewTask(async () => await Test(obj, worker))
        .OnError(Controller.Error);

    return true;
}

完整的游戏流程

csharp
private async Task Test(GamingObject obj, ModuleServerWorker worker)
{
    // ═══ 第一步:确认客户端连接 ═══
    Dictionary<string, object> data = [];
    data.Add("info_type", "check");
    Guid token = Guid.NewGuid();
    data.Add("connect_token", token);

    foreach (string username in obj.Users.Select(u => u.Username))
    {
        worker.UserData[username] = new() { { "connect_token", token } };
    }
    await SendGamingMessage(obj.All.Values, GamingType.UpdateInfo, data);

    // 等待所有玩家确认(30秒超时)
    await WaitForUsers(30,
        async () => worker.ConnectedUser.Count == obj.Users.Count,
        200,
        async () => await CancelGame(obj, worker, "等待超时,游戏已取消!"),
        async () => await StartGame(obj, worker)
    );
}

消息处理

csharp
public override async Task<Dictionary<string, object>> GamingMessageHandler(
    IServerModel model, GamingType type, Dictionary<string, object> data)
{
    ModuleServerWorker worker = Workers[model.InRoom.Roomid];
    string username = model.User.Username;

    switch (type)
    {
        case GamingType.Connect:
            // 验证 token
            string un = NetworkUtility.JsonDeserializeFromDictionary<string>(data, "username") ?? "";
            Guid token = NetworkUtility.JsonDeserializeFromDictionary<Guid>(data, "connect_token");
            if (un == username && worker.UserData[username]["connect_token"]?.Equals(token) ?? false)
            {
                worker.ConnectedUser.Add(obj.Users.First(u => u.Username == username));
            }
            break;

        case GamingType.PickCharacter:
            long id = NetworkUtility.JsonDeserializeFromDictionary<long>(data, "id");
            if (worker.CharactersForPick.FirstOrDefault(c => c.Id == id) is Character character)
            {
                worker.UserCharacters[username] = character.Copy(); // 复制防冲突
            }
            break;

        case GamingType.Skill:
            string e = NetworkUtility.JsonDeserializeFromDictionary<string>(data, "event") ?? "";
            if (e == "SelectSkillTargets")
            {
                long[] targets = NetworkUtility.JsonDeserializeFromDictionary<long[]>(data, "targets") ?? [];
                worker.UserData[username]["SkillTargets"] = targets;
            }
            break;
    }
    return [];
}

创建 GamingQueue 并运行

csharp
private async Task StartGame(GamingObject obj, ModuleServerWorker worker)
{
    // 让玩家选角色
    Character c1 = Factory.OpenFactory.GetInstance<Character>(1, "", []);
    worker.CharactersForPick.Add(c1);

    // 从 ModuleLoader 获取已加载的角色
    if (ModuleLoader?.Characters.Values.FirstOrDefault() is CharacterModule cm)
    {
        Character c2 = cm.Characters.Values.FirstOrDefault()?.Copy() ?? Factory.GetCharacter();
        if (c2.Id > 0) worker.CharactersForPick.Add(c2);
    }

    data["list"] = worker.CharactersForPick.Select(c => c.Id);
    await SendGamingMessage(obj.All.Values, GamingType.PickCharacter, data);

    // 等待选择完成...
    await WaitForUsers(30, /* ... */);

    // 准备角色
    List<Character> finalList = [.. worker.UserCharacters.Values];
    foreach (Character c in finalList)
    {
        c.Level = 60;
        c.NormalAttack.Level = 8;
        Skill s = Factory.OpenFactory.GetInstance<Skill>(1, "", []);
        s.Level = 6;
        c.Skills.Add(s);
    }

    // 创建队列
    MixGamingQueue queue = new(finalList, async (str) =>
    {
        await SendAllTextMessage(obj, str);
    });

    // 加载地图(可选)
    if (GameModuleDepend.Maps.FirstOrDefault() is GameMap map)
    {
        queue.LoadGameMap(map);
    }

    // 绑定事件
    queue.SelectSkillTargetsEvent += (q, caster, skill, ...) =>
    {
        return SyncAwaiter.WaitResult(
            RequestClientSelectSkillTargets(worker, caster, skill, ...));
    };

    queue.InitActionQueue();
    queue.SetCharactersToAIControl(cancel: false, finalList);

    // 游戏循环
    int i = 1;
    while (i < 999)
    {
        Character? characterToAct = queue.NextCharacter();
        if (characterToAct != null)
        {
            await SendAllTextMessage(obj, $"=== Round {i++} ===");
            await SendAllTextMessage(obj, $"现在是 [ {characterToAct} ] 的回合!");

            bool isGameEnd = queue.ProcessTurn(characterToAct);
            if (isGameEnd) break;
            queue.DisplayQueue();
        }
        queue.TimeLapse();
    }

    // 赛后统计
    foreach (Character character in queue.CharacterStatistics
        .OrderByDescending(d => d.Value.TotalDamage).Select(d => d.Key))
    {
        CharacterStatistics stats = queue.CharacterStatistics[character];
        // 输出各项统计...
    }
}

与客户端交互:选择技能目标

csharp
private async Task<List<Character>> RequestClientSelectSkillTargets(
    ModuleServerWorker worker, Character caster, Skill skill, ...)
{
    Dictionary<string, object> data = [];
    data.Add("event", "SelectSkillTargets");
    data.Add("caster", caster.Id);
    data.Add("skill", skill.Id);
    data.Add("allenemys", allEnemys.Select(c => c.Id));
    data.Add("teammates", teammates.Select(c => c.Id));
    await SendGamingMessage(_clientModels, GamingType.Skill, data);

    // 等待客户端返回选择结果
    List<Character> selectTargets = [];
    await WaitForUsers(30, /* 等待 UserData["SkillTargets"] */ );
    return selectTargets;
}

匿名服务器

支持客户端不经过 FunGameServer 登录验证直接连接:

csharp
public override bool StartAnonymousServer(IServerModel model,
    Dictionary<string, object> data)
{
    string access_token = NetworkUtility.JsonDeserializeFromDictionary<string>(
        data, "access_token") ?? "";
    if (access_token == "approval_access_token")
    {
        _clientModels.Add(model);
        return true;
    }
    return false;
}

public override async Task<Dictionary<string, object>> AnonymousGameServerHandler(
    IServerModel model, Dictionary<string, object> data)
{
    // 自定义协议处理...
    return [];
}

热重载 (IHotReloadAware)

csharp
public void OnBeforeUnload()
{
    // 清理所有匿名连接
    GamingObjects.Clear();
    foreach (IServerModel model in _clientModels)
    {
        model.NowGamingServer = null;
        CloseAnonymousServer(model);
    }
}

基于 LGPL-3.0 许可证发布