SmartQueue

SmartQueue

基于NeoForge平台的智能排队模块

SmartQueue

一个适用于 NeoForge 1.21.1 的智能玩家队列系统


概述

SmartQueue 用可配置的、基于优先级的玩家队列,取代了原版 Minecraft 的“服务器已满”拒绝机制。当服务器达到玩家上限时,新连接会被停放在 NeoForge 的配置阶段——他们会看到一个实时队列界面,显示排队位置和预计等待时间,并在有位置空出时自动被接纳进入游戏。工作人员和 VIP 玩家会获得优先位置和更快的接纳间隔,而断开连接的玩家其队列位置会在可配置的宽限期内得到保留,重新连接后无缝恢复。

功能特性

  • 可配置玩家上限 — 将 effective_max_players 设置为低于 server.properties 中的 max-players,以预留管理员位置或强制执行排队
  • VIP 专属位置 — 保留一部分服务器容量仅供 VIP 玩家使用,确保付费用户总能进入服务器
  • 工作人员专属位置 — 当启用工作人员绕过队列时,可在 effective_max_players 之上额外增加专供工作人员使用的位置,使工作人员加入不会占用普通玩家容量
  • 实时队列界面 — 显示排队位置、队列总人数、前方玩家数和预计等待时间
  • 队列详情显示 — 按队列细分显示每个队列(工作人员、优先重连、VIP、普通)的总人数和前方人数,可配置开关;实时更新反映实际调度顺序,包括比例模式和防失衡状态
  • 优先级层级 — 工作人员(最高)、VIP 和普通玩家,支持可配置的接纳模式
  • 比例接纳模式 — 可选基于比例的人数接纳(例如“3 个 VIP 然后 2 个普通玩家”),带防失衡保护以防止普通玩家被饿死
  • 首位锁定 — 调度顺序排在第一位的玩家被锁定,不会被新到达的更高优先级玩家挤掉,确保队列公平推进
  • 位置受阻指示器 — 当普通玩家到达队首但无法进入(VIP 专属位置已满)时,队列界面会显示明显的警告,而不是误导性地显示“下一个就是你!”
  • 接纳确定性显示 — /smartqueue status 详细视图会用绿色的 >> 标记保证的下一位玩家,用黄色的 ? 标记不确定的候选者(根据位置类型可能有多个下一位)
  • 四个独立队列 — 工作人员、优先重连、VIP 和普通队列,严格执行接纳顺序
  • 带位置恢复的重连 — 断开连接后在宽限期内返回即可保留排队位置
  • 重连限速 — 可配置在时间窗口内玩家可使用优先重连的次数上限,防止滥用重连系统
  • 断开连接位置保留 — 短暂断开的排队玩家可在可配置的宽限期内保留其位置;重新连接时无缝恢复,不会丢失位置或影响其他玩家的位置
  • 自动位置补位 — 每个游戏刻运行的安全网确保有玩家在等待时不会留下空位
  • 暂停 / 恢复 — 在维护期间冻结队列而不会踢出任何人
  • 完整国际化 — 包含英语(en_us)和简体中文(zh_cn)
  • 热重载配置 — 在服务器运行时编辑磁盘上的 TOML 文件;更改自动生效
  • 游戏内管理 — /smartqueue 命令可切换、暂停、查看状态和管理工作人员/VIP 列表,无需重启
  • 公共状态命令 — /smartqueue status 对所有玩家开放(无需权限),任何人都可以查看队列。OP 和工作人员(可配置)可看到包含玩家名和身份标签的完整详情;普通玩家只能看到仅含玩家名的简化视图——不显示 VIP/工作人员身份标签
  • 音效 — 进入队列、离开队列和被接纳进入服务器时有音频反馈

环境要求

组件 版本
Minecraft 1.21.1
NeoForge 21.1.248+
Java 21+

SmartQueue 需要在服务器端和客户端都安装。服务器端处理队列逻辑、接纳和优先级管理。客户端渲染队列界面 GUI 并处理“离开队列”按钮——这要求客户端上也存在模组代码。

安装

服务器端和客户端

  1. 从发布页面下载最新的 smartqueue-1.5.0-NeoForge-1.21.1.jar文件。
  2. 将其放置在服务器的 mods/ 目录和每个玩家的客户端 mods/ 目录中。
  3. 启动服务器。将在 config/ 目录生成三个配置文件:
    • smartqueue-server.toml — 队列设置
    • smartqueue-staff.toml — 工作人员用户名列表
    • smartqueue-vip.toml — VIP 用户名列表
  4. 根据需要编辑配置。更改会自动应用(无需重启)。

单人游戏 / 局域网

该模组也支持单人游戏。将 effective_max_players 设置为低于世界设置中的 maxPlayers,即可在本地世界测试队列。

工作原理

配置阶段

当玩家超过服务器的 effective_max_players 时,SmartQueue 会拦截 PlayerList.placeNewPlayer() 并取消原版的玩家放置。玩家不会加入世界,而是被停放在 NeoForge 的配置阶段——即登录和游戏之间的协议状态。

在此阶段:

  • 服务器会定期(每 5 秒)发送 QueueStatusPayload 数据包,包含玩家当前的位置、队列总人数、前方人数和预计等待时间
  • 客户端显示由模组在客户端渲染的 QueueScreen GUI
  • 对 ServerConfigurationPacketListenerImpl.tick() 的 Mixin 会重置原版超时计时器并移除 Netty 的 ReadTimeoutHandler,使连接可无限期保持

接纳机制

SmartQueue 支持两种接纳模式,由 proportional_mode 配置选项控制。

传统模式(proportional_mode = false,默认)

每个游戏刻运行两个独立的接纳计时器:

  • VIP 计时器(默认:每 40 刻 / 2 秒)— 接纳排在第一位的已排队工作人员或 VIP 玩家
  • 普通计时器(默认:每 100 刻 / 5 秒)— 接纳排在第一位的已排队普通玩家

两个计时器仅在 activeCount() < effective_max_players 时触发。每刻还会运行一个安全网,立即用最高优先级的等待玩家(工作人员 → 优先重连 → VIP → 普通)填满任何空位。

比例模式(proportional_mode = true)

运行单个接纳计时器(使用 normal_admit_interval_ticks),玩家按可配置的比例循环被接纳:

每个计时器刻的接纳顺序:
  1. 锁定条目        (队首锁定 — 不可被挤掉)
  2. 工作人员        (在剩余队列中始终优先,无配额)
  3. 优先重连        (WAS_PLAYING 重连,跳过不可接纳的普通玩家)
  4. 防失衡          (对被跳过的普通玩家的补偿 — 见下文)
  5. 比例循环        (VIP:普通 比例,交替进行)

比例循环维护一个阶段(VIP 或普通)和一个计数器:

  • VIP 阶段:接纳最多 proportional_vip_count 个 VIP,然后切换到普通阶段
  • 普通阶段:接纳最多 proportional_normal_count 个普通玩家,然后切换回 VIP 阶段
  • 如果队列为空,阶段会立即切换以避免浪费接纳机会
  • 安全网(玩家断开时的位置补位)也遵循比例阶段并正确更新阶段计数器,确保即使在玩家快速更替时也能维持比例

防失衡保护: 当比例循环进入普通阶段但普通位置已满(由于 vip_exclusive_slots),且同时有 VIP 和普通玩家在等待时:

  1. 跳过被计数:skippedNormalCount + 1
  2. 阶段立即切换回 VIP 以保持接纳流动
  3. 当普通位置稍后可用时,系统进入防失衡模式:接纳按照实际加入顺序(最早者优先,跨 VIP 和普通队列)进行,而非 VIP/普通比例
  4. 每个通过任何路径(锁定条目、比例循环或防失衡补偿)被接纳的普通玩家都会将 skippedNormalCount 减 1
  5. 防失衡期间的 VIP 接纳不影响计数器——只有普通玩家偿还债务
  6. 当 skippedNormalCount 达到 0 时,普通比例循环恢复
  7. 如果队列完全清空,skippedNormalCount 自动重置为 0

这确保了 VIP 永远不会完全饿死普通玩家——每次被跳过的普通接纳最终都会得到偿还,无论普通玩家走哪条接纳路径。

当被接纳时,玩家的 placeNewPlayer() 会被真实调用(通过 ThreadLocal<Boolean> ADMITTING 标志绕过 Mixin 守卫),队列界面关闭,玩家加入游戏世界。客户端只会看到统一的“前方 X 名玩家”计数——所有内部队列分离和比例逻辑对玩家不可见。

断开连接与超时保护

当排队玩家的连接断开时,SmartQueue 不会立即移除他们。相反,玩家的位置会在可配置的宽限期内(queue_disconnect_grace_ticks,默认 60 秒)在队列中保留。在此期间:

  • 断开的条目保留在队列列表中——其他玩家的位置保持稳定
  • 接纳跳过断开的条目;其后的下一位已连接玩家会被接纳
  • 如果玩家在宽限期内重新连接,其位置会无缝恢复(无需“重连”操作——同一位置会重新激活)
  • 如果宽限期到期,条目会被永久移除——玩家下次连接时必须重新排队
机制 位置 描述
断开事件 ServerConfigDisconnectMixin 捕获配置监听器上的 onDisconnect → 将条目标记为 DISCONNECTED(如果 queue_disconnect_grace_ticks = 0 则立即移除)
刻清理 QueueManager.cleanupDisconnected() 每个游戏刻遍历所有排队连接并将不活跃的连接标记为 DISCONNECTED
过期清理 QueueManager.cleanupExpiredDisconnected() 每个游戏刻移除宽限期已过期的 DISCONNECTED 条目

为防止原版踢出闲置的排队玩家:

机制 位置 描述
计时器重置 ConfigTickHeadMixin 每个游戏刻重置 keepAlivePending、keepAliveTime 和 closedListenerTime
超时移除 ConfigTickHeadMixin 从通道管线中移除 Netty 的 ReadTimeoutHandler(30 秒读取超时)

“离开队列”按钮

队列界面包含一个“离开队列”按钮。点击后:

  1. 客户端捕获活动的 Connection(在状态数据包到达时从 NeoForge 的 IPayloadContext 获取)
  2. 调用 Connection.disconnect() 关闭 TCP 通道
  3. 导航到标题画面
  4. 服务器检测到断开 → 将条目标记为 DISCONNECTED,在宽限期内保留位置

连接看门狗

客户端监控传入的 QueueStatusPayload 数据包以检测连接问题:

阶段 条件 行为
正常 数据包约每 5 秒到达 队列界面正常更新
警告 超过 30 秒无数据包 队列界面出现橙色 [!] 服务器连接丢失——等待恢复... 提示。位置和预计等待时间冻结在最后已知值。如果数据包恢复,提示自动清除。
连接已死 TCP 通道变为不活跃(例如服务器进程被终止) 客户端通过 Netty 通道状态立即检测到 !isConnected() 并返回标题画面——通常在服务器关闭后几秒内。
放弃 超过 60 秒无数据包 客户端断开连接并返回标题画面。这是 TCP 通道保持打开但服务器不发送数据(例如游戏刻线程挂起)时的回退方案。

如果服务器重启,客户端几乎立即检测到 TCP 通道已死(通过操作系统发送的 TCP RST)并返回标题画面。玩家可以立即重新连接,无需等待任何超时。但是,队列状态存储在服务器的内存中,因此服务器重启意味着所有队列位置和重连记录都会丢失——玩家需要重新开始。

配置

smartqueue-server.toml

所有值都在 [queue] 部分下。

键 类型 默认值 范围 描述
enabled 布尔 true — 总开关。为 false 时,所有已排队玩家立即被接纳,新玩家绕过队列。
effective_max_players 整数 20 1–1024 最大活跃(非排队)玩家数。设置为低于 server.properties 中的 max-players 以预留管理员位置或强制执行排队。
max_queue_size 整数 50 0–1024 队列中等待的最大玩家数。超过此数量的连接将被断开并显示“服务器已满”消息。
normal_admit_interval_ticks 整数 100 1–72000 接纳每个普通玩家之间的游戏刻数。20 刻 = 1 秒(默认:5 秒)。
vip_admit_interval_ticks 整数 40 1–72000 接纳每个工作人员/VIP 玩家之间的游戏刻数(默认:2 秒)。
rejoin_grace_ticks 整数 6000 0–1728000 WAS_PLAYING 重连的时间窗口:曾进入游戏、断开连接并在服务器满时重新连接的玩家会获得优先重连队列位置。0 = 禁用。默认:6000 刻(5 分钟)。
queue_disconnect_grace_ticks 整数 6000 0–72000 断开的排队玩家位置被保留的时间(游戏刻)。在此窗口内重新连接可无缝恢复。过期条目被永久移除。0 = 立即移除(不保留位置)。默认:6000 刻(5 分钟)。
staff_bypass_queue 布尔 false — 服务器满时工作人员的行为。false = 工作人员进入队列前端(优先插入)。true = 工作人员完全绕过队列直接加入。为 true 时,确保 effective_max_players 低于 server.properties 中的 max-players 以保留工作人员位置。可考虑使用 staff_exclusive_slots(见下文)而非降低 effective_max_players。
staff_exclusive_slots 布尔 false — 启用以工作人员为专属的额外位置。仅在 staff_bypass_queue = true 时生效。启用后,前 N 名工作人员玩家(由 staff_exclusive_slots_count 设置)不计入 effective_max_players,允许在不减少普通玩家位置的情况下为工作人员提供额外容量。
staff_exclusive_slots_count 整数 2 0–1024 工作人员专属额外位置的数量。仅在 staff_exclusive_slots = true 时使用。当 > 0 时,最多这么多工作人员不计入 effective_max_players。为 0 时,工作人员拥有无限专属位置(仅受 server.properties 中的 max-players 约束)。超过此数量的工作人员仍绕过队列,但占用普通玩家位置。
vip_exclusive_slots 整数 0 0–1024 专为 VIP 用户保留的位置数。当 > 0 时,非 VIP 玩家上限为 effective_max_players - vip_exclusive_slots。剩余位置只能由 VIP(以及工作人员,当 staff_bypass_queue=false 时)填充。示例:effective_max_players=35,vip_exclusive_slots=5 → 非 VIP 上限为 30。如果误配置高于 effective_max_players,该值会自动被钳制。
proportional_mode 布尔 false — 启用比例接纳模式。为 true 时,VIP 和普通玩家按可配置的比例被接纳(例如 3 个 VIP 然后 1 个普通玩家,交替进行)。工作人员始终优先。为 false 时,使用传统双计时器模式(VIP 和普通玩家各有独立的接纳间隔)。
proportional_vip_count 整数 2 1–100 每个比例循环接纳的 VIP 玩家数量。仅在 proportional_mode = true 时使用。
proportional_normal_count 整数 1 1–100 每个比例循环接纳的普通玩家数量。仅在 proportional_mode = true 时使用。
staff_see_detailed_status 布尔 true — 非 OP 工作人员是否可以看到包含玩家名和身份标签的详细队列状态。OP(权限级别 2+)始终可以看到完整视图。为 false 时,工作人员看到与普通玩家相同的简化视图。
show_queue_detail 布尔 true — 向客户端显示详细的队列细分。为 true 时,已排队玩家可以看到每个队列(工作人员、优先重连、VIP、普通)中有多少人以及根据实际调度顺序他们前方每种类型的数量。为 false 时,客户端只看到简单的位置编号。如果 staff_bypass_queue 为 true,工作人员队列行会从客户端隐藏。
rejoin_rate_limit_enabled 布尔 false — 启用以限速重连。为 true 时,重复使用优先重连跳过队列的玩家会被限速:如果他们在 rejoin_rate_limit_window_ticks 内超过 rejoin_rate_limit_max_count 次重连,后续重连将被视为新连接(无优先级)。当重连链断裂(玩家未在宽限期内重连)时计数器重置。
rejoin_rate_limit_window_ticks 整数 36000 1–1728000 重连限速的时间窗口(游戏刻)。默认:36000 刻 = 30 分钟。
rejoin_rate_limit_max_count 整数 3 1–1000 限速窗口内允许的最大优先重连次数。默认:3。

smartqueue-staff.toml

staff = ["Admin1", "OwnerName"]
  • 用户名不区分大小写
  • 工作人员玩家在队列中拥有最高优先级——排在 VIP 和普通玩家之前
  • 工作人员按VIP 间隔被接纳(比普通玩家快)

smartqueue-vip.toml

vip = ["Supporter1", "FriendName"]
  • 用户名不区分大小写
  • VIP 玩家拥有中等优先级——排在工作人员之后、普通玩家之前
  • VIP 按VIP 间隔被接纳(比普通玩家快)

热重载

所有三个配置文件都由 NeoForge 的内置配置监视器监控。在服务器运行时编辑任何 .toml 文件,更改会在几秒内生效。使用 /smartqueue reload 确认。

命令

所有管理命令需要权限级别 2(管理员)。/smartqueue status 对所有玩家开放。根命令:/smartqueue

队列控制

命令 描述
/smartqueue toggle on 启用队列
/smartqueue toggle off 禁用队列(立即接纳所有已排队玩家)
/smartqueue toggle 显示当前开/关状态
/smartqueue pause 暂停接纳(玩家保持排队,不进行新的接纳)
/smartqueue resume 恢复接纳(重置计时器,继续接纳)
/smartqueue reload 确认配置重载
/smartqueue status 显示队列状态。OP 和工作人员(可通过 staff_see_detailed_status 配置)可看到活跃玩家、总容量(含工作人员专属细分,格式为 X / Y (Z+N))、接纳模式和比例、VIP 专属位置使用情况、接纳确定性(绿色 >> 表示确定的下一位,黄色 ? 表示 VIP 和普通玩家都在等待但其中一种类型被位置阻挡时的不确定候选者)、队列总人数,以及四个队列部分(工作人员 / 优先重连 / VIP / 普通),包含玩家名和身份标签。普通玩家看到简化视图:活跃玩家、最大容量、队列总人数,以及两个合并队列——优先重连队列(断开重连)和普通队列(工作人员 + VIP + 普通合并)——只显示玩家名,不显示身份标签。

工作人员管理

命令 描述
/smartqueue staff add <名字> 将玩家添加到工作人员列表(最高优先级)。持久化到 smartqueue-staff.toml。
/smartqueue staff remove <名字> 从工作人员列表中移除玩家。持久化到文件。更新队列顺序。
/smartqueue staff list 列出所有工作人员条目

VIP 管理

命令 描述
/smartqueue vip add <名字> 将玩家添加到 VIP 列表(中等优先级)。持久化到 smartqueue-vip.toml。
/smartqueue vip remove <名字> 从 VIP 列表中移除玩家。持久化到文件。更新队列顺序。
/smartqueue vip list 列出所有 VIP 条目

队列优先级系统

SmartQueue 维护四个独立队列。接纳顺序严格为:

优先级 队列 描述
0 锁定条目 调度顺序中的第一位玩家被锁定——不会被新到达的更高优先级玩家挤掉。他们是否能真正进入会在接纳时检查(例如,被 VIP 专属位置阻挡的普通玩家会被跳过,直到有位置空出)。
1 工作人员队列 工作人员玩家(来自 smartqueue-staff.toml)。在剩余队列中始终先于其他所有队列被接纳。
2 优先重连队列 正在游戏、断开连接、然后在服务器满时重新连接的玩家(WAS_PLAYING 重连)。按 FIFO 顺序接纳(先重连者先进入)。此队列中的普通玩家在 VIP 专属位置已满时会被跳过。
3 VIP 队列 VIP 玩家(来自 smartqueue-vip.toml)。在比例模式下按 VIP:普通 比例被接纳。在传统模式下按较快的 VIP 间隔被接纳。
4 普通队列 所有其他玩家。在比例模式下按比例被接纳。在传统模式下按较慢的普通间隔被接纳。

队列放置

当玩家进入队列时:

场景 目标队列 位置
工作人员玩家 工作人员队列 前端(位置 0)
WAS_PLAYING 重连(非工作人员) 优先重连队列 末尾(FIFO)
VIP 玩家 VIP 队列 末尾
普通玩家 普通队列 末尾
  • 工作人员和 VIP 互斥——如果玩家同时是两者,工作人员优先。
  • 默认情况下(staff_bypass_queue = false),服务器满时工作人员和 VIP 玩家仍须排队;只是获得优先位置和更快的接纳,而非绕过。

工作人员绕过模式

当 staff_bypass_queue = true 时,工作人员玩家完全跳过队列直接加入服务器——即使服务器已“满”(按 effective_max_players 定义)。这允许工作人员无论玩家数量如何都能访问服务器。

重要提示: SmartQueue 的 canPlayerLogin Mixin 抑制了原版的“服务器已满”拒绝。这意味着工作人员可以将服务器推到 server.properties 中的 max-players 之上。例如,max-players=32、在线 32 人时,一名工作人员加入——服务器将达到 33/32 人。

建议: 使用 staff_exclusive_slots(见下文)为工作人员添加专用额外位置,而不会减少普通玩家容量。如果未使用专属位置,始终将 effective_max_players 设置为比 server.properties 中的 max-players 至少低 1–2 个位置。例如:

# server.properties
max-players = 34

# smartqueue-server.toml
effective_max_players = 32
staff_bypass_queue = true
staff_exclusive_slots = true
staff_exclusive_slots_count = 2

使用此设置:32 个普通位置 + 2 个工作人员专属位置 = 34 上限,工作人员不减少普通容量,且 server.properties 中的 max-players(设置为 34)永远不会被超过。

VIP 专属位置

当 vip_exclusive_slots 设置为大于 0 的值时,服务器容量的一部分专为 VIP 玩家保留。非 VIP 玩家上限为 effective_max_players - vip_exclusive_slots,剩余位置只能由 VIP 资格玩家占用。

工作原理——示例: effective_max_players = 35,vip_exclusive_slots = 5

场景 非 VIP 在线 VIP 资格在线 非 VIP 可加入? VIP 可加入?
服务器基本为空 20 3 是(20 < 30) 是(23 < 35)
非 VIP 上限已到 30 2 排队(30 ≥ 30) 是(32 < 35)
服务器已满 30 5 排队(30 ≥ 30) 排队(35 ≥ 35)

与 staff_bypass_queue 的交互:

  • staff_bypass_queue = false(默认):VIP 和工作人员都计入 VIP 专属位置。排队中的工作人员玩家在位置占用方面被视为“VIP 资格”。
  • staff_bypass_queue = true:只有 VIP 玩家计入 VIP 专属位置。工作人员完全绕过队列,不影响 VIP 位置计数(但他们确实占用服务器上的一个普通位置)。

自动钳制: 如果 vip_exclusive_slots 意外设置高于 effective_max_players,它会自动被钳制为 effective_max_players(将所有位置视为 VIP 专属)以防止误配置。

工作人员专属位置

当 staff_bypass_queue = true 且 staff_exclusive_slots = true 时,服务器可以在不减少普通玩家容量的情况下容纳额外的工作人员玩家。前 N 名工作人员(由 staff_exclusive_slots_count 配置)占用 effective_max_players 之上的专用额外位置。

工作原理——示例: effective_max_players = 32,staff_exclusive_slots_count = 2

场景 非工作人员在线 工作人员在线 实际玩家 非工作人员可加入? 工作人员可加入?
服务器未满 25 1 26 是(25 < 32) 是(绕过)
非工作人员达到上限 32 0 32 排队 是(绕过,使用位置 1/2)
工作人员在专属位置 32 2 34 排队(非工作人员=32) 是(绕过,但占用普通位置)
带 VIP 专属 27 + 5VIP 2 34 排队(非 VIP=27=上限) 是(绕过)

关键行为:

  • 普通玩家将 effective_max_players 视为服务器上限(例如 32)——工作人员专属位置对他们不可见
  • OP 和工作人员(当 staff_see_detailed_status = true)可看到带细分的总容量,例如 活跃玩家: 30 / 34 (32+2) 显示 32 基础 + 2 工作人员专属,以及详细的工作人员位置使用情况
  • staff_exclusive_slots_count = 0 意味着无限工作人员专属位置——所有工作人员都不受限制(仅受 server.properties 中的 max-players 约束)
  • 超过专属位置数量的工作人员仍绕过队列,但占用普通位置,减少普通玩家的容量
  • 专属位置中的工作人员不计入 VIP 专属位置占用——两种机制相互独立

建议: 将 server.properties 中的 max-players 设置为至少 effective_max_players + staff_exclusive_slots_count,以确保原版限制不会阻止工作人员。

断开连接与重连

SmartQueue 对两种断开连接采取不同处理:

1. 队列断开——位置保留

当玩家在队列中等待时断开连接,其位置会在 queue_disconnect_grace_ticks(默认 60 秒)内保留。在此窗口内重新连接可在同一位置无缝恢复。如果窗口到期,条目会被永久移除,玩家必须重新排队。

这由上文断开连接与超时保护中描述的位置保留机制处理。

2. 游戏内断开(WAS_PLAYING)——优先重连

当玩家正在游戏中、断开连接并在 rejoin_grace_ticks(默认 5 分钟)内重新连接到已满服务器时,他们会被放入优先重连队列——在工作人员之后、所有 VIP 和普通队列之前被接纳。

配置 默认值 用途
queue_disconnect_grace_ticks 6000(5分钟) 已排队玩家断开时的位置保留
rejoin_grace_ticks 6000(5分钟) 游戏内玩家断开的优先重连窗口

客户端体验

队列界面

当玩家连接且服务器已满时,他们会看到:

┌──────────────────────────────────────┐
│         服务器队列                    │
│                                      │
│    位置: 5 / 16                      │
│                                      │
│    --- 队列概览 ---                   │
│    工作人员:  1 总计, 0 前方         │
│    优先:      1 总计, 0 前方         │
│    VIP:       3 总计, 2 前方         │
│    普通:      6 总计, 2 前方         │
│                                      │
│    您前方有 4 名玩家                  │
│    预计等待: 35秒                    │
│                                      │
│    请稍候,您已在队列中。             │
│                                      │
│    请勿关闭游戏。                    │
│                                      │
│         [ 离开队列 ]                  │
└──────────────────────────────────────┘
  • 位置随玩家被接纳或离开实时更新
  • 当 show_queue_detail = true 时,按队列细分显示每个队列中的人数(总计)以及您前方每种类型的数量(根据实际调度顺序)
  • 预计等待时间根据前方工作人员/VIP/普通玩家的组合动态计算
  • 暂停时,标题变为“服务器队列 [已暂停]”并出现红色暂停提示
  • 当玩家到达位置 1 且可被接纳时,“下一个就是你!”(绿色)取代前方人数
  • 当玩家在位置 1 但被位置限制阻挡(例如 VIP 专属位置阻止普通玩家进入)时,显示“排在第一位,等待可用位置…”(黄色)
  • 按 ESC 无效 — 队列界面无法被意外关闭
  • 点击“离开队列”会断开连接并返回标题画面

音效

SmartQueue 在关键时刻播放音频反馈:

事件 音效 描述
进入队列 join_queue 玩家首次进入队列时播放(不是每次位置更新都播放)
离开队列 leave_queue 玩家自愿离开队列,或连接丢失/超时时播放
被接纳进服务器 queue_completed 玩家被接纳并加入游戏世界时播放

音效文件(.ogg)位于 assets/smartqueue/sounds/。要自定义音效,请替换这些文件或修改 sounds.json 以指向不同的音频资源。

玩家看到的流程

  1. 连接到已满的服务器
  2. 听到加入音效,看到位置“位置: 1 / 1”的队列界面
  3. 观看位置和预计等待时间随更多玩家加入而更新
  4. 位置达到“下一个就是你!”→ 听到接纳音效 → 游戏世界加载(如果被 VIP 专属限制阻挡,则显示“等待位置…”)

架构

┌──────────────────────────────────────────────────────────┐
│                    服务器端                              │
│                                                          │
│  PlayerListMixin (placeNewPlayer)                        │
│    │   需要排队?                                         │
│    ├──► QueueManager.enqueue() ──► 玩家停放在            │
│    │                               配置阶段              │
│    │                                                     │
│  QueueManager.onServerTick()                             │
│    ├── 清理断开/过期条目                                  │
│    ├── 接纳玩家(传统双计时器或比例)                     │
│    ├── 防失衡补偿(比例模式)                             │
│    └── 每 100 刻广播 QueueStatusPayload                   │
│                                                          │
│  ConfigTickHeadMixin                                     │
│    └── 重置 keepAlive 计时器 + 移除 Netty 超时            │
│                                                          │
│  ServerConfigDisconnectMixin (onDisconnect)              │
│    └── 标记 DISCONNECTED + 保留位置                      │
├──────────────────────────────────────────────────────────┤
│                    网络层                                │
│                                                          │
│  QueueStatusPayload (服务器 → 客户端, 配置阶段)           │
│    - 位置、总数、前方、已接纳、暂停、预计等待              │
│                                                          │
│  QueueActionPayload (客户端 → 服务器)                    │
│    - LEAVE_QUEUE (未使用; 客户端通过 TCP 断开)            │
├──────────────────────────────────────────────────────────┤
│                    客户端端                              │
│                                                          │
│  ClientQueueState.captureConnection()                    │
│    - 从网络上下文存储 Connection                          │
│                                                          │
│  ClientQueueState.update()                               │
│    - 更新位置/预计等待 → QueueScreen                      │
│                                                          │
│  QueueClientEvents.onClientTick()                        │
│    - 每个游戏刻重新断言 QueueScreen                       │
│                                                          │
│  QueueScreen                                             │
│    - 渲染位置、预计等待、离开按钮                         │
│    - onClose() 如果仍在排队则重新打开                     │
└──────────────────────────────────────────────────────────┘

Mixin

Mixin 目标 用途
PlayerListMixin PlayerList 覆盖“服务器已满”拒绝;拦截 placeNewPlayer 以入队
ServerConfigDisconnectMixin ServerConfigurationPacketListenerImpl 捕获已排队玩家的断开连接事件
ConfigTickHeadMixin ServerConfigurationPacketListenerImpl 重置 keepalive 计时器并移除 Netty ReadTimeoutHandler
ConfigTickMixin ServerCommonPacketListenerImpl keepAlivePending、keepAliveTime、closedListenerTime、connection 的访问器
ConnectionAccessor Connection Netty channel 的访问器
MinecraftAccessor Minecraft(客户端) pendingConnection 的访问器