bettertab

package module
v0.0.0-...-f67ae91 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

Gate BetterTab

Go Reference

An ultra-high performance, feature-packed TAB list & player list formatting extension for the Gate Minecraft proxy.


✨ Features

  • 🌈 Full RGB, MiniMessage & Legacy Formatting: Supports <rainbow>, <gradient:#from:#to>, <#RRGGBB>, <gold>, <b>, <i>, and classic &a, &l, &#RRGGBB color codes.
  • 🎬 Animated Headers & Footers: Define multiple frames in arrays — they cycle smoothly at your configured update interval.
  • 🏷️ Custom Server Groups: Group backend servers together and display completely unique TAB menus for each group with regex support.
  • 👑 Role System & Prefixes/Suffixes: Define custom prefixes, suffixes, and sorting weights for Admins, Mods, VIPs, and players.
  • 🔢 Native List Ordering & Sorting: Automatically sorts player entries by role hierarchy and username in Minecraft's TAB menu.
  • 🌐 Global or Per-Server Isolation: Choose whether players see all network players across all servers, or only players on their current backend server (only_list_players_in_same_server).
  • Zero Backend Plugins Required: Works 100% on the proxy level with zero overhead and lock-free thread safety.
  • 🔄 Live Hot-Reload: Update headers, footers, formats, and groups in-game with /bettertab reload.
  • 🎭 Custom Nicknames & Personal Toggles: Players can set personal tab display names with /bettertab name <name> or toggle tablist formatting with /bettertab toggle.

📦 Installation

In Pelican / Pterodactyl Panel

Add the plugin package to your Gate Go Plugins (List) variable:

github.com/andreisugu/gate-bettertab

(Combine with other plugins: github.com/andreisugu/gate-simplewhitelist, github.com/andreisugu/gate-hybridforwarding, github.com/andreisugu/gate-bettertab)

In Go Code
go get github.com/andreisugu/gate-bettertab
package main

import (
	"github.com/andreisugu/gate-bettertab"
	"go.minekube.com/gate/cmd/gate"
	"go.minekube.com/gate/pkg/edition/java/proxy"
)

func main() {
	proxy.Plugins = append(proxy.Plugins,
		bettertab.Plugin,
	)
	gate.Execute()
}

⚙️ Configuration (config/bettertab.toml)

# Main settings
update_interval_ms = 1000
sort_players = true
default_group = "default"

# Define roles and sorting priority (higher weight = higher in the TAB list)
[roles.owner]
prefix = "<dark_red>[Owner] </dark_red>"
weight = 1000

[roles.admin]
prefix = "<red>[Admin] </red>"
weight = 500

[roles.vip]
prefix = "<green>[VIP] </green>"
weight = 100

[roles.default]
prefix = "<gray>"
suffix = "</gray>"
weight = 0

# Assign players to roles
[player_roles]
# andrei = "owner"

# Server groups
[[groups]]
name = "default"
servers = ["*"]
only_list_players_in_same_server = false
sorting_placeholders = ["%role_weight%", "%username_lower%"]

# Animated header frames (cycles smoothly)
headers = [
    "<rainbow>═══ Minecraft Proxy Network ═══</rainbow>\n<gray>Welcome back, <white>%username%</white>!</gray>",
    "<rainbow>═══ Minecraft Proxy Network ═══</rainbow>\n<gray>Server: <aqua>%server%</aqua> | Ping: <green>%ping%ms</green></gray>",
]

# Animated footer frames
footers = [
    "<gray>Players Online: <green>%players_online%</green>/<dark_gray>%max_players_online%</dark_gray></gray>\n<gold>store.example.com</gold>",
    "<gray>Players on <aqua>%server%</aqua>: <yellow>%local_players_online%</yellow></gray>\n<aqua>discord.gg/example</aqua>",
]

# Tablist entry format
format = "<gray>[<aqua>%server%</aqua>]</gray> %prefix%<white>%username%</white>%suffix%"

# Custom replacements (e.g. pretty server badges)
[groups.placeholder_replacements]
"%server%" = { "smp" = "<green>SMP</green>", "lobby" = "<aqua>Lobby</aqua>", "survival" = "<gold>Survival</gold>", "limbo" = "<gray>Limbo</gray>" }

📌 Supported Placeholders

Placeholder Description
%username% Player's username (or custom name if set)
%username_lower% Lowercase player username
%server% Current backend server name (with replacements applied)
%raw_server% Raw backend server name without replacements
%server_group% Current active server group name
%ping% Player's connection ping in milliseconds
%ping_colored% Color-coded ping (green / yellow / red)
%players_online% Total online players connected to the proxy
%max_players_online% Max player limit set in proxy config
%local_players_online% Players on the current backend server
%server_online_players_<srv>% Online players on a specific server (e.g. %server_online_players_smp%)
%group_players_online% Players in the current tab group
%group_players_online_<grp>% Online players in a specific group
%current_date% Formatted current date (YYYY-MM-DD)
%current_time% Formatted current time (HH:mm:ss)
%prefix% Player's role prefix
%suffix% Player's role suffix
%role% Player's role name
%role_weight% Player's role sorting weight
%condition: A < B ? C : D% Conditional evaluation

🎮 In-Game Commands

Command Permission Description
/bettertab reload Console / Op Hot-reloads config/bettertab.toml with zero proxy restarts.
/bettertab name <name> All Sets a custom personal display name in the TAB list.
/bettertab reset All Resets custom TAB display name back to original username.
/bettertab toggle All Toggles custom TAB list formatting on/off for the player.
/bettertab info All Shows active server, group, role, ping, and online statistics.
/btab All Short alias for /bettertab.

📄 License

Apache 2.0 © Andrei

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Plugin = proxy.Plugin{
	Name: "BetterTab",
	Init: func(ctx context.Context, p *proxy.Proxy) error {
		log := logr.FromContextOrDiscard(ctx)
		configPath := filepath.Join("config", "bettertab.toml")

		store := NewConfigStore(configPath)
		if _, err := store.Load(); err != nil {
			log.Error(err, "Failed to load bettertab configuration")
			return err
		}

		states := NewPlayerStateManager()
		tabMgr := NewTabManager(p, store, states, log)
		tabMgr.Start(ctx)

		event.Subscribe(p.Event(), 0, func(e *proxy.ServerPostConnectEvent) {
			sName := "unknown"
			if e.Player().CurrentServer() != nil {
				sName = e.Player().CurrentServer().Server().ServerInfo().Name()
			}
			tabMgr.HandleServerSwitch(e.Player(), sName)
		})

		event.Subscribe(p.Event(), 0, func(e *proxy.DisconnectEvent) {
			tabMgr.HandlePlayerDisconnect(e.Player())
		})

		buildCommand := func(name string) brigodier.LiteralNodeBuilder {
			return brigodier.Literal(name).
				Executes(command.Command(func(cmdCtx *command.Context) error {
					return cmdCtx.Source.SendMessage(&c.Text{
						Content: "§b[BetterTab for Gate] §7by Andrei\n" +
							"§7Commands: §f/" + name + " reload§7, §f/" + name + " name <name>§7, §f/" + name + " reset§7, §f/" + name + " toggle§7, §f/" + name + " info",
					})
				})).
				Then(brigodier.Literal("reload").
					Requires(command.Requires(func(c *command.RequiresContext) bool {
						return c.Source.HasPermission("bettertab.reload") || c.Source.HasPermission("bettertab.admin") || c.Source.HasPermission("*")
					})).
					Executes(command.Command(func(cmdCtx *command.Context) error {
						if _, err := store.Load(); err != nil {
							return cmdCtx.Source.SendMessage(&c.Text{Content: fmt.Sprintf("§c[BetterTab] Reload failed: %v", err)})
						}
						tabMgr.UpdateAll()
						return cmdCtx.Source.SendMessage(&c.Text{Content: "§a[BetterTab] Configuration reloaded successfully!"})
					})),
				).
				Then(brigodier.Literal("reset").
					Requires(command.Requires(func(c *command.RequiresContext) bool {
						return c.Source.HasPermission("bettertab.reset") || c.Source.HasPermission("bettertab.name") || c.Source.HasPermission("bettertab.admin") || c.Source.HasPermission("*")
					})).
					Executes(command.Command(func(cmdCtx *command.Context) error {
						player, ok := cmdCtx.Source.(proxy.Player)
						if !ok {
							return cmdCtx.Source.SendMessage(&c.Text{Content: "§cOnly players can reset their TAB name."})
						}
						if states.ResetCustomName(player.ID()) {
							tabMgr.UpdateAll()
							return cmdCtx.Source.SendMessage(&c.Text{Content: "§a[BetterTab] Your TAB display name has been reset."})
						}
						return cmdCtx.Source.SendMessage(&c.Text{Content: "§e[BetterTab] You do not have a custom TAB name set."})
					})),
				).
				Then(brigodier.Literal("toggle").
					Executes(command.Command(func(cmdCtx *command.Context) error {
						player, ok := cmdCtx.Source.(proxy.Player)
						if !ok {
							return cmdCtx.Source.SendMessage(&c.Text{Content: "§cOnly players can toggle their TAB list."})
						}
						enabled := states.ToggleTab(player.ID())
						tabMgr.UpdateAll()
						if enabled {
							return cmdCtx.Source.SendMessage(&c.Text{Content: "§a[BetterTab] Custom TAB list enabled."})
						}
						return cmdCtx.Source.SendMessage(&c.Text{Content: "§e[BetterTab] Custom TAB list disabled."})
					})),
				).
				Then(brigodier.Literal("info").
					Executes(command.Command(func(cmdCtx *command.Context) error {
						player, ok := cmdCtx.Source.(proxy.Player)
						if !ok {
							cfg := store.GetConfig()
							return cmdCtx.Source.SendMessage(&c.Text{
								Content: fmt.Sprintf("§b[BetterTab Status] §7Groups: %d, Online: %d", len(cfg.Groups), len(p.Players())),
							})
						}
						sName := "none"
						if cs := player.CurrentServer(); cs != nil {
							sName = cs.Server().ServerInfo().Name()
						}
						group := store.GetGroupForServer(sName)
						gName := "none"
						if group != nil {
							gName = group.Name
						}
						_, rName := store.GetRoleWithInfo(player.Username())

						return cmdCtx.Source.SendMessage(&c.Text{
							Content: fmt.Sprintf("§b[BetterTab Info]\n§7Server: §f%s\n§7Group: §f%s\n§7Role: §f%s\n§7Ping: §f%dms",
								sName, gName, rName, player.Ping().Milliseconds()),
						})
					})),
				).
				Then(brigodier.Literal("name").
					Requires(command.Requires(func(c *command.RequiresContext) bool {
						return c.Source.HasPermission("bettertab.name") || c.Source.HasPermission("bettertab.admin") || c.Source.HasPermission("*")
					})).
					Then(brigodier.Argument("new_name", brigodier.String).
						Executes(command.Command(func(cmdCtx *command.Context) error {
							player, ok := cmdCtx.Source.(proxy.Player)
							if !ok {
								return cmdCtx.Source.SendMessage(&c.Text{Content: "§cOnly players can set their TAB name."})
							}
							newName := cmdCtx.String("new_name")
							if strings.TrimSpace(newName) == "" {
								return cmdCtx.Source.SendMessage(&c.Text{Content: "§cName cannot be empty."})
							}
							if (strings.Contains(newName, "&") || strings.Contains(newName, "§") || strings.Contains(newName, "<")) &&
								!player.HasPermission("bettertab.name.color") && !player.HasPermission("bettertab.admin") && !player.HasPermission("*") {
								return cmdCtx.Source.SendMessage(&c.Text{Content: "§cYou do not have permission to use color or formatting tags in your TAB name (bettertab.name.color)."})
							}
							states.SetCustomName(player.ID(), newName)
							tabMgr.UpdateAll()
							return cmdCtx.Source.SendMessage(&c.Text{
								Content: fmt.Sprintf("§a[BetterTab] Your TAB display name is now: §f%s", newName),
							})
						})),
					),
				)
		}

		p.Command().Register(buildCommand("bettertab"))
		p.Command().Register(buildCommand("btab"))
		p.Command().Register(
			brigodier.Literal("gatetab").
				Executes(command.Command(func(cmdCtx *command.Context) error {
					return cmdCtx.Source.SendMessage(&c.Text{
						Content: "§bUse §f/bettertab §b(or §f/btab§b) for tab list commands.",
					})
				})),
		)

		log.Info("BetterTab loaded (custom tablist formatting, animations & groups active)")
		return nil
	},
}

Plugin is the Gate BetterTab tablist & player list customization plugin.

Functions

func FormatText

func FormatText(input string) c.Component

FormatText parses MiniMessage, MineDown, and Legacy color syntax into a native Minecraft Component.

func ReplacePlaceholders

func ReplacePlaceholders(template string, ctx *PlaceholderContext) string

ReplacePlaceholders evaluates and replaces all built-in, contextual, and conditional placeholders.

Types

type Config

type Config struct {
	UpdateIntervalMs int               `toml:"update_interval_ms"`
	SortPlayers      bool              `toml:"sort_players"`
	DefaultGroup     string            `toml:"default_group"`
	Roles            map[string]Role   `toml:"roles"`
	PlayerRoles      map[string]string `toml:"player_roles"`
	Groups           []Group           `toml:"groups"`
}

Config represents the complete Velocitab configuration.

type ConfigStore

type ConfigStore struct {
	// contains filtered or unexported fields
}

ConfigStore provides thread-safe configuration management.

func NewConfigStore

func NewConfigStore(path string) *ConfigStore

NewConfigStore creates a new ConfigStore.

func (*ConfigStore) GetConfig

func (cs *ConfigStore) GetConfig() *Config

GetConfig returns the current Config snapshot.

func (*ConfigStore) GetGroupForServer

func (cs *ConfigStore) GetGroupForServer(serverName string) *Group

GetGroupForServer finds the matching Group for a given server name.

func (*ConfigStore) GetRoleForPlayer

func (cs *ConfigStore) GetRoleForPlayer(username string) Role

GetRoleForPlayer returns the role associated with a player name.

func (*ConfigStore) GetRoleWithInfo

func (cs *ConfigStore) GetRoleWithInfo(username string) (Role, string)

GetRoleWithInfo returns the role and its role key name.

func (*ConfigStore) Load

func (cs *ConfigStore) Load() (*Config, error)

Load loads or creates the configuration file.

type Group

type Group struct {
	Name                        string                       `toml:"name"`
	Servers                     []string                     `toml:"servers"`
	Headers                     []string                     `toml:"headers"`
	Footers                     []string                     `toml:"footers"`
	Format                      string                       `toml:"format"`
	OnlyListPlayersInSameServer bool                         `toml:"only_list_players_in_same_server"`
	SortingPlaceholders         []string                     `toml:"sorting_placeholders"`
	PlaceholderReplacements     map[string]map[string]string `toml:"placeholder_replacements"`
	// contains filtered or unexported fields
}

Group represents a server group tablist definition.

func (*Group) MatchesServer

func (g *Group) MatchesServer(serverName string) bool

MatchesServer checks if a server matches any of the group's server patterns.

type PlaceholderContext

type PlaceholderContext struct {
	Player           proxy.Player
	CustomName       string
	ServerName       string
	ProxyPlayerCount int
	MaxPlayers       int
	LocalPlayerCount int
	GroupPlayerCount int
	ServerCounts     map[string]int
	GroupCounts      map[string]int
	Role             Role
	RoleName         string
	Group            *Group
}

PlaceholderContext provides metadata for placeholder evaluation.

type PlayerStateManager

type PlayerStateManager struct {
	// contains filtered or unexported fields
}

PlayerStateManager tracks in-memory custom names and preferences for players.

func NewPlayerStateManager

func NewPlayerStateManager() *PlayerStateManager

NewPlayerStateManager creates a new PlayerStateManager.

func (*PlayerStateManager) GetCustomName

func (sm *PlayerStateManager) GetCustomName(id uuid.UUID) (string, bool)

GetCustomName returns the custom nickname if present.

func (*PlayerStateManager) IsTabDisabled

func (sm *PlayerStateManager) IsTabDisabled(id uuid.UUID) bool

IsTabDisabled checks if custom tab formatting is disabled for a player.

func (*PlayerStateManager) RemovePlayer

func (sm *PlayerStateManager) RemovePlayer(id uuid.UUID)

RemovePlayer cleans up player state on disconnect.

func (*PlayerStateManager) ResetCustomName

func (sm *PlayerStateManager) ResetCustomName(id uuid.UUID) bool

ResetCustomName clears any custom nickname for a player.

func (*PlayerStateManager) SetCustomName

func (sm *PlayerStateManager) SetCustomName(id uuid.UUID, name string)

SetCustomName sets a custom display nickname for a player.

func (*PlayerStateManager) ToggleTab

func (sm *PlayerStateManager) ToggleTab(id uuid.UUID) bool

ToggleTab toggles custom tab formatting for a player and returns the new state (true = enabled, false = disabled).

type Role

type Role struct {
	Prefix string `toml:"prefix"`
	Suffix string `toml:"suffix"`
	Weight int    `toml:"weight"`
}

Role represents prefix, suffix, and sorting weight.

type TabManager

type TabManager struct {
	// contains filtered or unexported fields
}

TabManager manages the live state, animation ticks, and tablist synchronization for all players.

func NewTabManager

func NewTabManager(p *proxy.Proxy, store *ConfigStore, states *PlayerStateManager, log logr.Logger) *TabManager

NewTabManager creates a new TabManager instance.

func (*TabManager) HandlePlayerDisconnect

func (tm *TabManager) HandlePlayerDisconnect(player proxy.Player)

HandlePlayerDisconnect cleans up state and updates remaining players upon logout.

func (*TabManager) HandleServerSwitch

func (tm *TabManager) HandleServerSwitch(player proxy.Player, targetServer string)

HandleServerSwitch immediately updates tablist state upon switching servers.

func (*TabManager) Start

func (tm *TabManager) Start(ctx context.Context)

Start begins the background update loop.

func (*TabManager) UpdateAll

func (tm *TabManager) UpdateAll()

UpdateAll synchronizes headers, footers, display names, and list ordering across all active connections.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL