Skip to content

Simple Proxy • Example

Simple Proxy is a runnable example Go project that showcases all the basic features of Gate. You can use this project as a template for your own Gate plugins.

Running the Simple Proxy

sh
git clone https://github.com/minekube/gate.git
cd gate/.examples/extend/simple-proxy
go run .

Run any Minecraft backend server

You must run a backend server to actually join the game and see Simple Proxy in action. Open a new terminal and start a Minecraft server:

sh
cd .examples/simple-network/server1
java -jar *.jar -nogui

Join the proxy at localhost:25565.

Learning Task

If you change code in the Simple Proxy project, you must restart the proxy to see the changes. (press CTRL+C to stop the proxy)

Add a /playerinfo command to the proxy that prints the player details like username and play duration.

Solution
go
package examples

import (
	"fmt"
	"math"
	"sync"
	"time"

	"github.com/robinbraemer/event"
	"go.minekube.com/brigodier"
	"go.minekube.com/common/minecraft/component"
	"go.minekube.com/gate/pkg/command"
	"go.minekube.com/gate/pkg/edition/java/proxy"
	"go.minekube.com/gate/pkg/util/uuid"
)

// call this in your plugin init function
func initPlayerInfo(p *proxy.Proxy) {
	subscribeJoinTime(p)
	registerPlayerInfoCommand(p)
}

func registerPlayerInfoCommand(p *proxy.Proxy) {
	p.Command().Register(playerInfoCommand())
}

func playerInfoCommand() brigodier.LiteralNodeBuilder {
	return brigodier.Literal("playerinfo").
		Executes(command.Command(func(c *command.Context) error {
			player, ok := c.Source.(proxy.Player)
			if !ok {
				return c.SendMessage(&component.Text{Content: "Only players can use this command."})
			}
			return c.SendMessage(playerInfo(player))
		}))
}

func playerInfo(p proxy.Player) *component.Text {
	state.RLock()
	joinTime := state.joinTime[p.ID()]
	state.RUnlock()

	msg := fmt.Sprintf("Your name is %s and you joined %s",
		p.Username(), time.Since(joinTime).Round(time.Second))

	return &component.Text{Content: msg}
}

func subscribeJoinTime(p *proxy.Proxy) {
	event.Subscribe(p.Event(), math.MaxInt, saveJoinTime)
	event.Subscribe(p.Event(), math.MinInt, deleteJoinTime)
}

var state = struct {
	joinTime map[uuid.UUID]time.Time
	sync.RWMutex
}{
	joinTime: make(map[uuid.UUID]time.Time),
}

func saveJoinTime(e *proxy.PostLoginEvent) {
	state.Lock()
	defer state.Unlock()
	state.joinTime[e.Player().ID()] = time.Now()
}

func deleteJoinTime(e *proxy.DisconnectEvent) {
	state.Lock()
	defer state.Unlock()
	delete(state.joinTime, e.Player().ID())
}

Code

You can check out the project on GitHub.

go
// Simple example embedding and extending Gate.
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/robinbraemer/event"
	"go.minekube.com/brigodier"
	"go.minekube.com/common/minecraft/color"
	. "go.minekube.com/common/minecraft/component"
	"go.minekube.com/common/minecraft/component/codec/legacy"
	"go.minekube.com/gate/cmd/gate"
	"go.minekube.com/gate/pkg/command"
	"go.minekube.com/gate/pkg/edition/java/bossbar"
	"go.minekube.com/gate/pkg/edition/java/proxy"
	"go.minekube.com/gate/pkg/edition/java/title"
)

func main() {
	// Add our "plug-in" to be initialized on Gate start.
	proxy.Plugins = append(proxy.Plugins, proxy.Plugin{
		Name: "SimpleProxy",
		Init: func(ctx context.Context, proxy *proxy.Proxy) error {
			return newSimpleProxy(proxy).init()
		},
	})

	// Execute Gate entrypoint and block until shutdown.
	// We could also run gate.Start if we don't need Gate's command-line.
	gate.Execute()
}

// SimpleProxy is a simple proxy to showcase some features of Gate.
//
// In this example:
//   - Add a `/broadcast` command
//   - Send a message when player switches the server
//   - Show boss bars to players
type SimpleProxy struct {
	*proxy.Proxy
}

var legacyCodec = &legacy.Legacy{Char: legacy.AmpersandChar}

func newSimpleProxy(proxy *proxy.Proxy) *SimpleProxy {
	return &SimpleProxy{
		Proxy: proxy,
	}
}

// initialize our sample proxy
func (p *SimpleProxy) init() error {
	p.registerCommands()
	p.registerSubscribers()
	return nil
}

// Register a proxy-wide commands (can be run while being on any server)
func (p *SimpleProxy) registerCommands() {
	// Registers the "/broadcast" command
	p.Command().Register(brigodier.Literal("broadcast").Then(
		// Adds message argument as in "/broadcast <message>"
		brigodier.Argument("message", brigodier.StringPhrase).
			// Adds completion suggestions as in "/broadcast [suggestions]"
			Suggests(command.SuggestFunc(func(
				c *command.Context,
				b *brigodier.SuggestionsBuilder,
			) *brigodier.Suggestions {
				player, ok := c.Source.(proxy.Player)
				if ok {
					b.Suggest("&oI am &6&l" + player.Username())
				}
				b.Suggest("Hello world!")

				// We can also order suggestion based on the current input and sort them.
				// The slice provides available suggestion candidates.
				//return suggest.Similar(b, []string{"FirstTry", "SecondTry"}).Build()

				return b.Build()
			})).
			// Executed when running "/broadcast <message>"
			Executes(command.Command(func(c *command.Context) error {
				// Colorize/format message
				message, err := legacyCodec.Unmarshal([]byte(c.String("message")))
				if err != nil {
					return c.Source.SendMessage(&Text{
						Content: fmt.Sprintf("Error formatting message: %v", err)})
				}

				// Send to all players on this proxy
				for _, player := range p.Players() {
					// Send message in new goroutine to not block
					// this loop if any player has a slow connection.
					go func(p proxy.Player) { _ = p.SendMessage(message) }(player)
				}
				return nil
			})),
	))
	p.Command().Register(brigodier.Literal("ping").
		Executes(command.Command(func(c *command.Context) error {
			player, ok := c.Source.(proxy.Player)
			if !ok {
				return c.Source.SendMessage(&Text{Content: "Pong!"})
			}
			return player.SendMessage(&Text{
				Content: fmt.Sprintf("Pong! Your ping is %s", player.Ping()),
				S:       Style{Color: color.Green},
			})
		})),
	)
	p.Command().Register(titleCommand())
}

func titleCommand() brigodier.LiteralNodeBuilder {
	showTitle := command.Command(func(c *command.Context) error {
		player, ok := c.Source.(proxy.Player)
		if !ok {
			return c.Source.SendMessage(&Text{Content: "You must be a player to run this command."})
		}

		ti, err := legacyCodec.Unmarshal([]byte(c.String("title")))
		if err != nil {
			return player.SendMessage(&Text{
				Content: fmt.Sprintf("Error parsing title: %v", err),
			})
		}

		// empty if arg not provided
		subtitle, err := legacyCodec.Unmarshal([]byte(c.String("subtitle")))
		if err != nil {
			return player.SendMessage(&Text{
				Content: fmt.Sprintf("Error parsing title: %v", err),
			})
		}

		return title.ShowTitle(player, &title.Options{
			Title:    ti,
			Subtitle: subtitle,
		})
	})

	return brigodier.Literal("title").
		Then(brigodier.Argument("title", brigodier.String).Executes(showTitle).
			Then(brigodier.Argument("subtitle", brigodier.StringPhrase).Executes(showTitle)))
}

// Register event subscribers
func (p *SimpleProxy) registerSubscribers() {
	// Send message on server switch.
	event.Subscribe(p.Event(), 0, p.onServerSwitch)

	// Change the MOTD response.
	event.Subscribe(p.Event(), 0, pingHandler())

	// Show a boss bar to all players on this proxy.
	event.Subscribe(p.Event(), 0, p.bossBarDisplay())
}

func (p *SimpleProxy) onServerSwitch(e *proxy.ServerPostConnectEvent) {
	newServer := e.Player().CurrentServer()
	if newServer == nil {
		return
	}

	_ = e.Player().SendMessage(&Text{
		S: Style{Color: color.Aqua},
		Extra: []Component{
			&Text{
				Content: "\nWelcome to the Gate Sample proxy!\n\n",
				S:       Style{Color: color.Green, Bold: True},
			},
			&Text{Content: "You connected to "},
			&Text{Content: newServer.Server().ServerInfo().Name(), S: Style{Color: color.Yellow}},
			&Text{Content: "."},
			&Text{
				S: Style{
					ClickEvent: SuggestCommand("/broadcast Gate is awesome!"),
					HoverEvent: ShowText(&Text{Content: "/broadcast Gate is awesome!"}),
				},
				Content: "\n\nClick me to run ",
				Extra: []Component{&Text{
					Content: "/broadcast Gate is awesome!",
					S:       Style{Color: color.White, Bold: True, Italic: True},
				}},
			},
			&Text{
				Content: "\n\nClick me to run sample /title command!",
				S: Style{
					HoverEvent: ShowText(&Text{Content: "/title <title> [subtitle]"}),
					ClickEvent: SuggestCommand(`/title "&eGate greets" &2&o` + e.Player().Username()),
				},
			},
			&Text{Content: "\n\nMore sample commands you can try: "},
			&Text{
				Content: "/ping",
				S:       Style{Color: color.Yellow},
			},
		},
	})
}

func pingHandler() func(p *proxy.PingEvent) {
	motd := &Text{Content: "Simple Proxy!\nJoin and test me."}
	return func(e *proxy.PingEvent) {
		p := e.Ping()
		p.Description = motd
		p.Players.Max = p.Players.Online + 1
	}
}

func (p *SimpleProxy) bossBarDisplay() func(*proxy.LoginEvent) {
	// Create shared boss bar for all players
	sharedBar := bossbar.New(
		&Text{Content: "Welcome to Gate Sample proxy!", S: Style{
			Color: color.Aqua,
			Bold:  True,
		}},
		1,
		bossbar.BlueColor,
		bossbar.ProgressOverlay,
	)

	updateBossBar := func(bar bossbar.BossBar, player proxy.Player) {
		now := time.Now()
		text := &Text{Extra: []Component{
			&Text{
				Content: fmt.Sprintf("Hello %s! ", player.Username()),
				S:       Style{Color: color.Yellow},
			},
			&Text{
				Content: fmt.Sprintf("It's %s", now.Format("15:04:05 PM")),
				S:       Style{Color: color.Gold},
			},
		}}
		bar.SetName(text)
		bar.SetPercent(float32(now.Second()) / 60)
	}

	return func(e *proxy.LoginEvent) {
		if !e.Allowed() {
			return
		}
		player := e.Player()

		// Add player to shared boss bar
		_ = sharedBar.AddViewer(player)

		// Create own boss bar for player
		playerBar := bossbar.New(
			&Text{},
			bossbar.MinProgress,
			bossbar.RedColor,
			bossbar.ProgressOverlay,
		)
		// Show it to player
		_ = playerBar.AddViewer(player)

		// Update boss bars every second until player disconnects.
		// Run in new goroutine to unblock login event handler!
		go tick(player.Context(), time.Second, func() {
			updateBossBar(playerBar, player)
		})
	}
}

// tick runs a function every interval until the context is cancelled.
func tick(ctx context.Context, interval time.Duration, fn func()) {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			fn()
		case <-ctx.Done():
			return
		}
	}
}

Released under the MIT License. (web version: 23d050a5)