Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cookies #507

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

feat: add cookies #507

wants to merge 1 commit into from

Conversation

BorisP1234
Copy link

@BorisP1234 BorisP1234 commented Feb 20, 2025

Hello, cookie packets were introduced in Minecraft 1.20.5 along with transfer packets, an underrated feature that can be particularly useful in multi-proxy scenarios. That’s why I found it unfortunate that this functionality wasn’t included in the Gate API, so I decided to add it myself!

I added three functions to the player variable:

  • player.CookieStore(key key.Key, payload []byte) error
    This function sends a cookie to the player, storing it even between transfers until the player disconnects. It returns an error if:

    • The key is empty.
    • The payload is too large.
    • The player's state is not "play" or "config."
    • The player is using a version lower than 1.20.5.
    • An error occurs while sending the packet.
  • player.CookieRequest(key key.Key) error
    This function requests a cookie from the player. The player then sends a response packet, which can be handled with the PlayerCookieResponseEvent (explained later). It returns an error if:

    • The key is empty.
    • The player is using a version lower than 1.20.5.
    • An error occurs while sending the packet.
  • player.CookieRequestWithResult(key key.Key) ([]byte, error)
    This function retrieves a cookie immediately instead of relying on PlayerCookieResponseEvent. It waits for a response from the player for up to 5 seconds. It returns an error if:

    • The key is empty.
    • The player is using a version lower than 1.20.5.
    • An error occurs while sending the packet.
    • The response takes longer than 5 seconds.

I added a new event called PlayerCookieResponseEvent, which listens for cookie response packets. This event contains the player, key, and payload. If the requested cookie is not found, an empty payload is returned.

You can test the features with this wonderful mod that I found called: CookieJar by Tis_awesomeness.

@BorisP1234
Copy link
Author

BorisP1234 commented Mar 3, 2025

Hi, hoping someone takes a look at the pr. To show an example of how someone would use cookies I created a couple functions which will be used in my multi proxy setup, communicating with Redis. I will be able to send players to any server under any proxy that uses this system.

package transfer

import (
	"context"
	"errors"
	"strings"
	"sync"
	"time"

	"github.com/redis/go-redis/v9"
	"github.com/team-vesperis/vesperis-mp/mp/database"
	"github.com/team-vesperis/vesperis-mp/mp/share"
	"go.minekube.com/common/minecraft/color"
	"go.minekube.com/common/minecraft/component"
	"go.minekube.com/common/minecraft/key"
	"go.minekube.com/gate/pkg/edition/java/proxy"
	"go.uber.org/zap"
)

var (
	p           *proxy.Proxy
	logger      *zap.SugaredLogger
	pubsub      *redis.PubSub
	ctx         context.Context
	cancel      context.CancelFunc
	wg          sync.WaitGroup
	proxy_name  string
	transferKey key.Key
)

func InitializeTransfer(proxy *proxy.Proxy, log *zap.SugaredLogger, pn string) {
	p = proxy
	logger = log
	proxy_name = pn
	transferKey = key.New("vmp", "transfer")
	ctx, cancel = context.WithCancel(context.Background())
	go listenToTransfers()
	logger.Info("Initialized transfer.")
}

// send players to other proxies
func OnPreShutdown() func(*proxy.PreShutdownEvent) {
	return func(event *proxy.PreShutdownEvent) {
		for _, player := range p.Players() {
			for _, proxy := range share.GetAllProxyNames() {
				if proxy != proxy_name {
					err := TransferPlayerToProxy(player, proxy)
					if err == nil {
						return
					}
				}
			}

			player.Disconnect(&component.Text{
				Content: "The proxy you were on has closed and there was no other proxy to connect to.",
				S: component.Style{
					Color: color.Red,
				},
			})

		}
	}
}

// check if player has cookie specifying which server he needs.
func OnChooseInitialServer() func(*proxy.PlayerChooseInitialServerEvent) {
	return func(event *proxy.PlayerChooseInitialServerEvent) {
		if len(p.Servers()) < 1 {
			event.Player().Disconnect(&component.Text{
				Content: "No available server. Please try again.",
				S: component.Style{
					Color: color.Red,
				},
			})
		} else {
			payload, err := event.Player().RequestCookieWithResult(transferKey)
			if err == nil {
				server_name := string(payload)
				server := p.Server(server_name)
				if server != nil {
					event.SetInitialServer(server)
				}
			} else {
				event.SetInitialServer(p.Servers()[0])
			}
		}
	}
}

func TransferPlayerToServerOnOtherProxy(player proxy.Player, targetProxy string, targetServer string) error {
	pubsub := database.GetRedisClient().Subscribe(context.Background(), "proxy_transfer_accept")
	defer pubsub.Close()

	err := database.GetRedisClient().Publish(context.Background(), "proxy_transfer_request", player.ID().String()+"|"+targetProxy+"|"+targetServer).Err()
	if err != nil {
		logger.Error("Error publishing transfer command: ", err)
		return err
	}

	ch := pubsub.Channel()
	timeout := time.After(2 * time.Second)

	for {
		select {
		case msg := <-ch:
			parts := strings.Split(msg.Payload, "|")
			if len(parts) == 4 && parts[0] == player.ID().String() && parts[1] == targetProxy {
				// server will be one of three things:
				// 0, means the proxy is found but given server is not available
				// 1, means the proxy is found and none server is specified
				// 2, means the proxy is found and the given server is available
				server := parts[3]

				logger.Info(server)
				if server == "0" {
					logger.Warn("Specified server for player transfer was not found: ", player.ID().String())
					return errors.New("specified server was not found")
				}

				if server == "2" {
					// store cookie
					payload := []byte(targetServer)
					err := player.StoreCookie(transferKey, payload)
					if err != nil {
						logger.Warn("Could not store cookie for player transfer for: ", player.ID().String())
						return errors.New("could not store cookie")
					}
				}

				address := parts[2]
				err := player.TransferToHost(address)
				if err != nil {
					return err
				}

				logger.Info("Player transfer successful: ", player.ID().String(), " to ", address)
				return nil
			}
		case <-timeout:
			logger.Warn("Timeout waiting for player transfer confirmation: ", player.ID().String())
			return errors.New("timeout waiting for player transfer confirmation")
		}
	}
}

func TransferPlayerToProxy(player proxy.Player, targetProxy string) error {
	return TransferPlayerToServerOnOtherProxy(player, targetProxy, "-")
}

func listenToTransfers() {
	pubsub = database.GetRedisClient().Subscribe(ctx, "proxy_transfer_request")

	for {
		msg, err := pubsub.ReceiveMessage(ctx)
		if err != nil {
			if err == context.Canceled || ctx.Err() == context.Canceled {
				return
			}
			logger.Error("Error receiving transfer command: ", err)
			continue
		}
		wg.Add(1)

		parts := strings.Split(msg.Payload, "|")
		if len(parts) != 3 {
			logger.Error("Invalid transfer command format")
			continue
		}

		playerID := parts[0]
		targetProxy := parts[1]
		targetServer := parts[2]
		address := p.Config().Bind

		if targetProxy == proxy_name {
			server := "0"
			if targetServer != "-" {
				server = "1"
			} else {
				foundServer := p.Server(targetServer)
				if foundServer != nil {
					server = "2"
				}
			}

			logger.Info(server)

			err := database.GetRedisClient().Publish(ctx, "proxy_transfer_accept", playerID+"|"+targetProxy+"|"+address+"|"+server).Err()
			if err != nil {
				logger.Warn("Error returning transfer. ", err)
			}
		}

		wg.Done()
	}
}

func CloseTransfer() {
	logger.Info("Closing transfer...")
	if cancel != nil {
		cancel()
	}
	wg.Wait()
	if pubsub != nil {
		pubsub.Close()
		logger.Info("Successfully closed transfer.")
	}
}

@robinbraemer
Copy link
Member

Thank you! I'll try to merge when I find the time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants