Files
FenceProxy/main.go
T

239 lines
6.1 KiB
Go

package main
import (
"context"
"fmt"
"log"
"net"
"os"
"strconv"
"time"
"github.com/go-redis/redis/v9"
"github.com/hashicorp/consul/api"
"go.minekube.com/brigodier"
. "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/proxy"
"github.com/robinbraemer/event"
)
func main() {
// Add our "plug-in" to be initialized on Gate start.
proxy.Plugins = append(proxy.Plugins, proxy.Plugin{
Name: "FenceProxy",
Init: func(ctx context.Context, proxy *proxy.Proxy) error {
return newFenceProxy(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()
}
// FenceProxy is a simple proxy that adds a `/broadcast` command
// and sends a message on server switch.
type FenceProxy struct {
*proxy.Proxy
legacyCodec *legacy.Legacy
}
func newFenceProxy(proxy *proxy.Proxy) *FenceProxy {
return &FenceProxy{
Proxy: proxy,
legacyCodec: &legacy.Legacy{Char: legacy.AmpersandChar},
}
}
// initialize our sample proxy
func (p *FenceProxy) init() error {
p.registerCommands()
go p.registerServers()
return p.registerSubscribers()
}
func (p *FenceProxy) registerServers() {
// Get a new client
config := api.DefaultConfig()
config.Address = getEnv("CONSUL_ADDR", "127.0.0.1:8500")
client, err := api.NewClient(config)
if err != nil {
panic(err)
}
// Get handle to catalog API
catalog := client.Catalog()
q := &api.QueryOptions{RequireConsistent: true}
var lastServers []proxy.ServerInfo
for {
allSvc, _, err := catalog.Services(q)
if err != nil {
log.Printf("Error querying Consul: %v", err)
}
var minecraftServices []string
for svc, tags := range allSvc {
for _, tag := range tags {
if tag == "consulity.enabled" {
minecraftServices = append(minecraftServices, svc)
break
}
}
}
var servers []proxy.ServerInfo
for _, name := range minecraftServices {
svcs, _, err := catalog.Service(name, "", q)
if err != nil {
log.Printf("Error querying Consul: %v", err)
continue
}
for _, svc := range svcs {
adr := fmt.Sprintf("%v:%d", svc.ServiceAddress, svc.ServicePort)
ip, err := net.ResolveTCPAddr("tcp", adr)
if err != nil {
log.Printf("Error converting address: %s", adr)
continue
}
servers = append(servers, proxy.NewServerInfo(svc.ID, ip))
}
}
// servers in lastServers that don't exist in servers now
var notExistingServers []proxy.ServerInfo
for _, server := range lastServers {
stillExists := false
for _, curServer := range servers {
if curServer.Name() == server.Name() {
stillExists = true
}
}
if !stillExists {
fmt.Printf("Server doesn't exist anymore: %v", server.Name())
notExistingServers = append(notExistingServers, server)
}
}
for _, server := range notExistingServers {
p.Unregister(server)
}
for _, server := range servers {
alreadyExists := false
for _, lastServer := range lastServers {
if lastServer.Name() == server.Name() {
alreadyExists = true
}
}
if !alreadyExists {
p.Register(server)
}
}
lastServers = servers
time.Sleep(5 * time.Second)
}
}
// Register a proxy-wide commands (can be run while being on any server)
func (p *FenceProxy) 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!")
return b.Build()
})).
// Executed when running "/broadcast <message>"
Executes(command.Command(func(c *command.Context) error {
// Colorize/format message
message, err := p.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 halt loop on slow connections.
go func(p proxy.Player) { _ = p.SendMessage(message) }(player)
}
return nil
})),
))
}
var ctx = context.Background()
var localPlayers int64 = 0
func getRedis() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: getEnv("REDIS_ADDR", "localhost:6379"),
Password: "", // no password set
DB: 0, // use default DB
})
return rdb
}
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
}
}
// Register event subscribers
func (p *FenceProxy) registerSubscribers() error {
rdb := getRedis()
event.Subscribe(p.Event(), 0, func(e *proxy.PingEvent) {
p := e.Ping()
val, err := rdb.Get(ctx, "players").Result()
if err == redis.Nil {
// does not exist yet
rdb.Set(ctx, "players", 0, 0)
} else if err != nil {
panic(err)
}
p.Players.Online, _ = strconv.Atoi(val)
p.Players.Max = p.Players.Online + 1
})
//p.Event().Subscribe(&proxy.LoginEvent{}, 0, func(ev event.Event) {
//e := ev.(*proxy.LoginEvent)
//e.Deny(&Text{Content: "&cMaintenance mode"})
//})
event.Subscribe(p.Event(), 0, func(e *proxy.PlayerChooseInitialServerEvent) {
e.SetInitialServer(p.Servers()[0])
localPlayers += 1
rdb.Incr(ctx, "players")
})
event.Subscribe(p.Event(), 0, func(e *proxy.DisconnectEvent) {
localPlayers -= 1
rdb.Decr(ctx, "players")
})
event.Subscribe(p.Event(), 0, func(e *proxy.ShutdownEvent) {
rdb.DecrBy(ctx, "players", localPlayers)
})
return nil
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}