diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..08b60d8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Build and Release + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: "1.22.4" + + - name: Build Windows executable + run: GOOS=windows GOARCH=amd64 go build -o vanilla-proxy-win.exe . + + - name: Build Linux executable + run: GOOS=linux GOARCH=amd64 go build -o vanilla-proxy-linux . + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.run_number }} + release_name: Release ${{ github.run_number }} + draft: false + prerelease: false + + - name: Upload Windows Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./vanilla-proxy-win.exe + asset_name: vanilla-proxy-win.exe + asset_content_type: application/octet-stream + + - name: Upload Linux Release Asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./vanilla-proxy-linux + asset_name: vanilla-proxy-linux + asset_content_type: application/octet-stream diff --git a/.gitignore b/.gitignore index acbe023..9c79769 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /whitelist.json -/config.toml \ No newline at end of file +/config.toml +/playerlist.json +/playerlist.json.lock \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9093651 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "cSpell.words": [ + "Debugf", + "Debugln", + "Errorln", + "gophertunnel", + "hujson", + "iconcache", + "Infoln", + "logrus", + "mathgl", + "Overworld", + "playerlist", + "pokeb", + "pokebedrock", + "raknet", + "Respawn", + "Runtimes", + "sandertv", + "Warnf", + "Warnln", + "XUID" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 9e747c7..48eed1d 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,94 @@ -## How to start a proxy server -1. Download and install [Go](https://go.dev/dl/) -2. Download and run Minecraft [Bedrock Dedicated Server](https://www.minecraft.net/en-us/download/server/bedrock). +> [!WARNING] +> # This Project has been Archived, and is no longer maintained. For a more up to date proxy, with way more features, please use [GoBDS](https://github.com/smell-of-curry/gobds) + +# Vanilla Proxy + +Vanilla Proxy is a simple proxy servers that enabled Packet management and protection of BDS server attacks through a end to end proxy system. + +## Overview + +1. You have all the advantages of the original BDS. +2. You can modify and cancel any packets passing through the proxy using the handler system. +3. You can organize the protection of your server from DDOS attacks. +4. You can set the boards of the world. Blocks outside the border will not be displayed to the player. The coordinates of the corner must be divided by 16 so that the borders are displayed correctly. +5. Support for a whitelist that can store the player's xbox id(xuid). +6. A Player List manager that stores a players `SelfSignedID` ensuring as long as a player is signed into a XBL account, they wont get there player reset. +7. A priority slot system, that allows you to block off specific slots to be restricted for certain players in whitelist. + +## Getting Started -```You must set online-mode=false in BDS server.properties``` +> [!IMPORTANT] +> You must configure BDS `server.properties` to disable `online-mode` and `client-side-chunk-generation-enabled` +1. Download and install [Go](https://go.dev/dl/) +2. Download and run Minecraft [Bedrock Dedicated Server](https://www.minecraft.net/en-us/download/server/bedrock). 3. Run in the console: + ``` -git clone https://github.com/HyPE-Network/vanilla-proxy +git clone https://github.com/smell-of-curry/vanilla-proxy cd vanilla-proxy go run main.go ``` ->You can also customize config.yml for yourself +## Configuration -## Overview -1. You have all the advantages of the original BDS. -2. You can modify and cancel any packets passing through the proxy using the handler system. -3. You can organize the protection of your server from ddos attacks. -4. You can set the boards of the world. Blocks outside the border will not be displayed to the player. The coordinates of the corner must be divided by 16 so that the borders are displayed correctly. -5. Support for a whitelist that can store the player's xbox id(xuid). -6. You can use a special bot for Windows with operator capabilities or send commands directly to screen on your Linux server. Screen must have the name of the original server port (for example 19132). -7. You can use rcon to execute commands automatically (let's say /whitelist add "nickname"). -8. A convenient API for creating forms, commands and fake inventories that are processed on the proxy side. +Configuration in the Vanilla Proxy is all managed through the [config.toml](config.toml.example). This file holds all details including database, server connection, api, world border and more. +To get started, copy the [config.toml.example](config.toml.example) file and rename it to `config.toml`. Inside this file you can first set the `Connection` properties. + +### Connection + +These are the most important as this is what the proxy uses to send the upstream connections too. + +- `ProxyAddress` - This is the address that the THIS proxy server will run on. This is the address that you want people to connect too. +- `RemoteAddress` - This is the address that the BDS server is running on. Ensure that the BDS server is running on a different port. + +### Api + +This vanilla proxy uses a API to fetch and set players connection details. Details such as connection IP, name, and XUID are saved for moderation capabilities. + +- `ApiHost` - This is the API host, something like `https://pokebedrock.com` would work. +- `ApiKey` - This is a authentication token which will be passed as a password. + +### Database + +This proxy uses a database to fetch claims, and local player details that are important for operations. This must be managed to ensure the claims work correctly. + +- `Host` - This is the host of the database, usually localhost `http://127.0.0.1` +- `Key` - This is the authentication key, which is passed as a password to ensure a authenticated session. +- `Name` - This is the database name you want to connect too, it would switch between servers, something like `black`, `white`, `testing` is used. +- `Port` - This is the database port that the server is running on, which will attach to `Host` when creating a request. + +### Logging + +This proxy uses a bit of logging so that the discord and the staff team are updated on the server details. Details about sign changes, and failed to ping alerts get sent to discord webhooks to ensure the staff is alerted. + +- `DiscordChatLogsWebhook` - this is the endpoint you want chat logs sent too. +- `DiscordCommandLogsWebhook` - this is the endpoint where when players use commands get sent too. +- `DiscordSignLogsWebhook` this is the destination for where sign edit logs should be sent. +- `DiscordStaffAlertsWebhook` - this is a endpoint for staff alerts, things like failed to ping database, etc. + +### Resources + +This is still a work in progress feature, however this configuration can allow use of custom resource packs to be downloaded by players. + +- `PackURLs` - this is a string array of URLs that the players must download and activate to play. +- `PackPaths` - this is a string array of pack paths that link to a folder that the player must download to play (used for local development). + +### Server + +Server holds less essential server configuration that changes connection aspects. + +- `SecuredSlots` - A count of how many slots to reduce from BDS max player count, that will be reserved for players in the whitelist allowing a priority slot system. +- `DisableXboxAuth` - specifies if authentication of players that join is disabled. If set to true, no verification will be done to ensure that the player connecting is authenticated using their XBOX Live account. +- `Prefix` - Prefix is used to specify the current server in error logs. For example `TESTING` would be sent with a logging endpoint to tell readers this came from `TESTING` server. +- `ViewDistance` - Manages the distance players can view through the chunk handler. This is important for large servers. +- `Whitelist` - If the whitelist is turned on and limiting players from joining. Whitelist can be managed through the (whitelist.json)[whitelist.json] file. +- `FlushRate` - The flush rate of the packets, default is `50` == `time.Second / 20`, the lower the value, the more CPU usage. + +### WorldBorder -## Examples -Example of a simple server with survival - [vanilla-survival](https://github.com/HyPE-Network/vanilla-survival) -> The world has borders, there is a ban on placing obsidian blocks in nether (players can teleport to distant coordinates using nether portals). -> This prohibition is made by processing the breaking of the block. +The proxy system comes with a pre-built world border that limits chunks, entities, and ticking from happening outside the world border. This is important for large servers and servers that pregenerate chunks. -## Recommendations -You can block the BDS server port using a firewall so that unauthorized players cannot enter the main server by bypassing the proxy. +- `Enabled` - if the world border is enabled. +- `MaxX` & `MaxZ` - Holds the Max location in the positive direction for the border, example `6000` +- `MinX` & `MinZ` - Holds the Minimum location in the negative direction for the border, example `-6000` diff --git a/config.toml.example b/config.toml.example new file mode 100644 index 0000000..a4d1a52 --- /dev/null +++ b/config.toml.example @@ -0,0 +1,46 @@ +[Connection] +ProxyAddress = "0.0.0.0:19132" # The port that players will connect to +RemoteAddress = "0.0.0.0:19134" # The port that the BDS server is hosted on + +[Server] +Whitelist = true # Whether to enable whitelisting +SecuredSlots = 5 # The amount of secured slots on the server for only whitelisted players +ViewDistance = 10 # The view distance for players +DisableXboxAuth = false # Whether to disable Xbox authentication (This cant be enabled currently) +FlushRate = 10 # The flush rate for the server +Prefix = "TESTING" # The prefix for the server, used for logging and discord messages + +[WorldBorder] +Enabled = true # Whether to enable the world border +MinX = -12000 # The minimum X coordinate for the world border +MaxX = 12000 # The maximum X coordinate for the world border +MinZ = -12000 # The minimum Z coordinate for the world border +MaxZ = 12000 # The maximum Z coordinate for the world border + +[Api] +ApiHost = "https://pokebedrock.com" # The host for the moderation API +ApiKey = "xxxxxxxx-xxxxxxxx-xxxxxxxx" # The API key for the moderation API +XboxApiKey = "xxxxxxxx-xxxxxxxx-xxxxxxxx" # The Xbox API key for profile picture fetching + +[Resources] +PackURLs = [] # Urls of resource packs to require downloaded by players +PackPaths = [] # Paths of resource packs to require downloaded by players + +[Database] +Host = "http://127.0.0.1:3000" # The host for the database +Key = "xxxxxxxx-xxxxxxxx-xxxxxxxx" # The key for the database +Name = "testing" # The name of the database (Should be close to Server.Prefix) + +[Encryption] +Key = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # The key for the encryption + +[Logging] +# Enables Discord logging for chat, command and sign logs +DiscordLoggingEnabled = false +DiscordChatLogsWebhook = "https://discord.com/api/webhooks/1111111111111111/xxxxxxxx-xxxxxxxx-xxxxxxxx" +DiscordCommandLogsWebhook = "https://discord.com/api/webhooks/1111111111111111/xxxxxxxx-xxxxxxxx-xxxxxxxx" +DiscordSignLogsWebhook = "https://discord.com/api/webhooks/1111111111111111/xxxxxxxx-xxxxxxxx-xxxxxxxx" +DiscordStaffAlertsWebhook = "https://discord.com/api/webhooks/1111111111111111/xxxxxxxx-xxxxxxxx-xxxxxxxx" +ProfilerHost = "127.0.0.1:19135" # The host for the profiler to view the server performance +# SentryDsn is optional - remove or leave empty to disable Sentry error tracking +SentryDsn = "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@xxxxxxx.ingest.sentry.io/xxxxxxx" diff --git a/custom_handlers/availableCommandsHandler.go b/custom_handlers/availableCommandsHandler.go new file mode 100644 index 0000000..8de6599 --- /dev/null +++ b/custom_handlers/availableCommandsHandler.go @@ -0,0 +1,80 @@ +package custom_handlers + +import ( + "strings" + + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type AvailableCommandsHandler struct { +} + +type CommandRequestHandler struct { +} + +// RemoveCommands removes a list of specified commands from the list of available commands. +func RemoveCommands(commands []protocol.Command, remove []string) []protocol.Command { + removeMap := make(map[string]bool) + for _, cmd := range remove { + removeMap[cmd] = true + } + + filteredCommands := make([]protocol.Command, 0, len(commands)) + for _, command := range commands { + if !removeMap[command.Name] { + filteredCommands = append(filteredCommands, command) + } + } + + return filteredCommands +} + +func (AvailableCommandsHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.AvailableCommands) + + // Define an array of command names to remove + commandsToRemove := []string{"me", "tell"} + dataPacket.Commands = RemoveCommands(dataPacket.Commands, commandsToRemove) + + proxy.ProxyInstance.Worlds.SetBDSAvailableCommands(dataPacket) + + return true, dataPacket, nil +} + +func (CommandRequestHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.CommandRequest) + playerData := player.GetData().GameData + + var command = strings.ToLower(strings.Split(dataPacket.CommandLine[1:], " ")[0]) + + if command == "me" || command == "tell" || command == "w" || command == "msg" { + player.SendMessage("§cThe command /" + command + " is disabled!") + player.PlaySound("note.bass", playerData.PlayerPosition, 1, 1) + return false, pk, nil + } + + minecraftCommands := proxy.ProxyInstance.Worlds.BDSAvailableCommands.Commands + // Check if {command} is a name inside {minecraftCommands} + for _, cmd := range minecraftCommands { + if cmd.Name == command { + return true, pk, nil + } + } + + // Command should be a custom `-` command + textPk := &packet.Text{ + TextType: packet.TextTypeChat, + NeedsTranslation: false, + SourceName: player.GetName(), + Message: "-" + strings.TrimPrefix(dataPacket.CommandLine, "/"), + Parameters: []string{}, + XUID: player.GetSession().IdentityData.XUID, + PlatformChatID: "", + } + player.DataPacketToServer(textPk) + + return false, dataPacket, nil +} diff --git a/custom_handlers/chatLogging.go b/custom_handlers/chatLogging.go new file mode 100644 index 0000000..e4d5d9f --- /dev/null +++ b/custom_handlers/chatLogging.go @@ -0,0 +1,49 @@ +package custom_handlers + +import ( + "fmt" + "strings" + + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/HyPE-Network/vanilla-proxy/utils" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type ChatLoggingHandler struct{} + +func (h ChatLoggingHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.Text) + + // Only care if player is sending message + if dataPacket.TextType != packet.TextTypeChat { + return true, pk, nil + } + + // Check if its a packet from the client + if dataPacket.SourceName != player.GetName() { + return true, pk, nil + } + + // Ignore commands + if strings.HasPrefix(dataPacket.Message, "-") || strings.HasPrefix(dataPacket.Message, "/") { + return true, pk, nil + } + + avatar_url, err := utils.GetXboxIconLink(player.GetSession().IdentityData.XUID, proxy.ProxyInstance.Config.Api.XboxApiKey) + if err != nil { + avatar_url = "https://media.forgecdn.net/avatars/121/268/636409261203329160.png" + } + + // Log message to discord. + utils.SendJsonToDiscord(proxy.ProxyInstance.Config.Logging.DiscordChatLogsWebhook, map[string]any{ + "username": fmt.Sprintf("[%s] %s", proxy.ProxyInstance.Config.Server.Prefix, player.GetName()), + "avatar_url": avatar_url, + "content": dataPacket.Message, + "allowed_mentions": map[string]any{ + "parse": []string{}, + }, + }) + + return true, pk, nil +} diff --git a/custom_handlers/claimHandler.go b/custom_handlers/claimHandler.go new file mode 100644 index 0000000..6480156 --- /dev/null +++ b/custom_handlers/claimHandler.go @@ -0,0 +1,214 @@ +package custom_handlers + +import ( + "strconv" + + "github.com/HyPE-Network/vanilla-proxy/log" + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/HyPE-Network/vanilla-proxy/utils" + "github.com/sandertv/gophertunnel/minecraft" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type VectorXZ struct { + X float32 `json:"x"` + Z float32 `json:"z"` +} + +type Location struct { + Dimension string `json:"dimension"` + Pos1 VectorXZ `json:"pos1"` + Pos2 VectorXZ `json:"pos2"` +} + +type IPlayerClaim struct { + ClaimId string `json:"claimId"` + PlayerXUID string `json:"playerXUID"` + Location Location `json:"location"` + Trusts []string `json:"trusts"` +} + +var RegisteredClaims (map[string]IPlayerClaim) + +func FetchClaims() error { + claims, err := utils.FetchDatabase[IPlayerClaim]("claims") + if err != nil { + return err + } + + RegisteredClaims = claims + log.Logger.Info("Loaded claims from database", "count", len(RegisteredClaims)) + + return nil +} + +// Dimension is the ID of the dimension that the player spawns in. It is a value from 0-2, with 0 being +// the overworld, 1 being the nether and 2 being the end. +func ClaimDimensionToInt(dimension string) int32 { + if dimension == "minecraft:overworld" { + return 0 + } else if dimension == "minecraft:nether" { + return 1 + } else if dimension == "minecraft:end" { + return 2 + } else { + return -1 + } +} + +// PlayerInsideClaim checks if a player is inside a claim +func PlayerInsideClaim(playerData minecraft.GameData, claim IPlayerClaim) bool { + dimensionInt := ClaimDimensionToInt(claim.Location.Dimension) + if dimensionInt != playerData.Dimension { + return false + } + Pos1X, Pos1Z := float32(claim.Location.Pos1.X), float32(claim.Location.Pos1.Z) + Pos2X, Pos2Z := float32(claim.Location.Pos2.X), float32(claim.Location.Pos2.Z) + + if playerData.PlayerPosition.X() >= Pos1X && playerData.PlayerPosition.X() <= Pos2X { + if playerData.PlayerPosition.Z() >= Pos1Z && playerData.PlayerPosition.Z() <= Pos2Z { + return true + } + } + + return false +} + +func getClaimAt(dimension int32, x, z int32) IPlayerClaim { + for _, claim := range RegisteredClaims { + if ClaimDimensionToInt(claim.Location.Dimension) == dimension { + Pos1X, Pos1Z := int32(claim.Location.Pos1.X), int32(claim.Location.Pos1.Z) + Pos2X, Pos2Z := int32(claim.Location.Pos2.X), int32(claim.Location.Pos2.Z) + + if x >= Pos1X && x <= Pos2X { + if z >= Pos1Z && z <= Pos2Z { + return claim + } + } + } + } + + return IPlayerClaim{} +} + +func canPreformActionInClaim(player human.Human, claim IPlayerClaim, action string) bool { + // if player.GetData().GameData.PlayerPermissions == 2 { + // return true + // } + + if action == "interactWithBlock" && claim.PlayerXUID == "*" { + // Players can interact with blocks in admin claims + return true + } + + if action == "interactWithEntity" && claim.PlayerXUID == "*" { + // Players can interact with entities in admin claims + return true + } + + playerXuid := player.GetSession().IdentityData.XUID + + if claim.PlayerXUID == playerXuid || utils.StringInSlice(playerXuid, claim.Trusts) { + return true + } + + return false +} + +type ClaimPlayerAuthInputHandler struct{} + +func (ClaimPlayerAuthInputHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.PlayerAuthInput) + + if dataPacket.Tick%20 == 0 { + playerPing := player.GetPing() + formattedPing := strconv.FormatUint(uint64(playerPing), 10) + pingStatus := "§a" + if playerPing < 20 { + pingStatus = "§a" + } else if playerPing < 50 { + pingStatus = "§e" + } else if playerPing < 100 { + pingStatus = "§6" + } else if playerPing < 200 { + pingStatus = "§c" + } else { + pingStatus = "§4" + } + titlePk := &packet.SetTitle{ + ActionType: packet.TitleActionSetTitle, + Text: "&_playerPing:Current Ping: " + pingStatus + formattedPing, + } + player.DataPacket(titlePk) + } + + // Loop through block actions, and check if player can interact with block + for _, blockAction := range dataPacket.BlockActions { + claim := getClaimAt(player.GetData().GameData.Dimension, int32(blockAction.BlockPos.X()), int32(blockAction.BlockPos.Z())) + if claim.ClaimId == "" { + continue + } + actionName := "interactWithBlock" + if blockAction.Action == 1 { + actionName = "breakBlock" + } + if !canPreformActionInClaim(player, claim, actionName) { + return false, pk, nil + } + } + + return true, pk, nil +} + +type ClaimInventoryTransactionHandler struct { +} + +func (ClaimInventoryTransactionHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.InventoryTransaction) + + playerData := player.GetData().GameData + + switch td := dataPacket.TransactionData.(type) { + case *protocol.UseItemTransactionData: + // Can stop players from using items like GUI, Pokedex, Potions, etc + // So use this to stop players from throwing projectiles + // This cannot stop interactions with entities or blocks. + + if td.HeldItem.Stack.ItemType.NetworkID == 0 { + // Using hand + return true, pk, nil + } + + claim := getClaimAt(player.GetData().GameData.Dimension, int32(td.Position.X()), int32(td.Position.Z())) + if claim.ClaimId == "" { + return true, pk, nil + } + if canPreformActionInClaim(player, claim, "useItem") { + return true, pk, nil + } + + item := proxy.ProxyInstance.Worlds.GetItemEntry(td.HeldItem.Stack.ItemType.NetworkID) + if item == nil { + // Item not sent over, most likely a minecraft item + return true, pk, nil + } + itemComponents := proxy.ProxyInstance.Worlds.GetItemComponentEntry(item.Name) + if itemComponents == nil { + // Item does not have any components, most-likely a minecraft item + return true, pk, nil + } + + if components, ok := itemComponents.Data["components"].(map[string]any); ok { + if _, ok := components["minecraft:throwable"]; ok { + // Item is throwable, stop the player from using it + player.SendMessage("§cYou cannot use throwable items in this claim!") + player.PlaySound("note.bass", playerData.PlayerPosition, 1, 1) + return false, pk, nil + } + } + } + + return true, pk, nil +} diff --git a/custom_handlers/customCommandRegisterHandler.go b/custom_handlers/customCommandRegisterHandler.go new file mode 100644 index 0000000..3c52767 --- /dev/null +++ b/custom_handlers/customCommandRegisterHandler.go @@ -0,0 +1,295 @@ +package custom_handlers + +import ( + "encoding/json" + "math" + "strings" + + "github.com/HyPE-Network/vanilla-proxy/log" + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/command" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +// --- New enum definition for command types --- + +type EngineResponseCommandType string + +const ( + EngineResponseCommandTypeLiteral EngineResponseCommandType = "literal" + EngineResponseCommandTypeString EngineResponseCommandType = "string" + EngineResponseCommandTypeInt EngineResponseCommandType = "int" + EngineResponseCommandTypeFloat EngineResponseCommandType = "float" + EngineResponseCommandTypeLocation EngineResponseCommandType = "location" + EngineResponseCommandTypeBoolean EngineResponseCommandType = "boolean" + EngineResponseCommandTypePlayer EngineResponseCommandType = "player" + EngineResponseCommandTypeTarget EngineResponseCommandType = "target" + EngineResponseCommandTypeArray EngineResponseCommandType = "array" + EngineResponseCommandTypeDuration EngineResponseCommandType = "duration" + EngineResponseCommandTypePlayerName EngineResponseCommandType = "playerName" +) + +// enumIndexShift: we use 0 here (i.e. no bit shifting) so that the enum index is ORed directly. +const enumIndexShift = 0 + +// getDerivationSuffix returns the expected suffix for literal/array derivation rule keys. +// For most commands, "f0" is used; if a particular command requires a special suffix (for example, "schedule"), +// adjust this function. +func getDerivationSuffix(name string) string { + lower := strings.ToLower(name) + if lower == "schedule" { + return "f2" + } + return "f0" +} + +// lowerOptions forces all option strings to lowercase. +func lowerOptions(options []string) []string { + result := make([]string, len(options)) + for i, o := range options { + result[i] = strings.ToLower(o) + } + return result +} + +// --- Update IEngineResponseCommand to use the new enum type --- + +type IEngineResponseCommand struct { + BaseCommand string `json:"baseCommand"` + Name string `json:"name"` + Description string `json:"description"` + Aliases []string `json:"aliases,omitempty"` + Type EngineResponseCommandType `json:"type"` + AllowedTypeValues []string `json:"allowedTypeValues,omitempty"` + Children []IEngineResponseCommandChild `json:"children"` + CanBeCalled bool `json:"canBeCalled"` + RequiresOp bool `json:"requiresOp"` +} + +type IEngineResponseCommandChild struct { + IEngineResponseCommand + Parent string `json:"parent"` + Depth int `json:"depth"` +} + +type IMinecraftRawText struct { + Text string `json:"text"` +} + +type IMinecraftTextMessage struct { + RawText []IMinecraftRawText `json:"rawtext"` +} + +type commandEnum struct { + // Type is the derivation rule key for this parameter (must be lowercase). + Type string + Options []string + Dynamic bool +} + +// valueToParamType maps a parameter's type to a protocol.CommandParameter base type plus optional enum data. +// For literal, boolean, and array types we return 0 and build a derivation rule key. +// For array, we now incorporate the parent's name if available so that the key is unique. +func valueToParamType(child IEngineResponseCommandChild) (t uint32, en commandEnum) { + switch child.Type { + case EngineResponseCommandTypeLiteral: + lit := strings.ToLower(child.Name) + return 0, commandEnum{ + Type: lit + "_" + getDerivationSuffix(child.Name), + Options: []string{lit}, + } + case EngineResponseCommandTypeString: + return protocol.CommandArgTypeString, en + case EngineResponseCommandTypeInt: + return protocol.CommandArgTypeInt, en + case EngineResponseCommandTypeFloat: + return protocol.CommandArgTypeFloat, en + case EngineResponseCommandTypeLocation: + return protocol.CommandArgTypePosition, en + case EngineResponseCommandTypeBoolean: + return 0, commandEnum{ + Type: "bool", + Options: []string{"true", "false"}, + } + case EngineResponseCommandTypePlayer: + return protocol.CommandArgTypeTarget, en + case EngineResponseCommandTypeTarget: + return protocol.CommandArgTypeTarget, en + case EngineResponseCommandTypeArray: + var baseKey string + // If a Parent is provided, use it to differentiate this array parameter. + if child.Parent != "" { + baseKey = strings.ToLower(child.Parent) + "_" + strings.ToLower(child.Name) + } else { + baseKey = strings.ToLower(child.Name) + } + return 0, commandEnum{ + Type: baseKey + "_" + getDerivationSuffix(child.Name), + Options: lowerOptions(child.AllowedTypeValues), + } + case EngineResponseCommandTypeDuration: + return protocol.CommandArgTypeString, en + case EngineResponseCommandTypePlayerName: + return protocol.CommandArgTypeString, en + default: + return protocol.CommandArgTypeString, en + } +} + +// flatParam represents a single token (literal or argument) in the command. +type flatParam struct { + name string // e.g. "set", "level", "pokemontype" + t uint32 // base type (with CommandArgValid flag) + en commandEnum +} + +// flattenBranches recursively traverses the command tree and returns a slice of branches. +// Each branch is a slice of flatParam representing one complete overload. +// In this function, if a token's name ends with "_y*" or "_z*", we skip it so that composite +// location arguments (like pos1, pos1_y*, pos1_z*) are not split. +func flattenBranches(prefix []flatParam, children []IEngineResponseCommandChild) [][]flatParam { + var branches [][]flatParam + if len(children) == 0 { + return [][]flatParam{prefix} + } + for _, child := range children { + lowerName := strings.ToLower(child.Name) + // Skip tokens that are extra coordinate parts. + if strings.HasSuffix(lowerName, "_y*") || strings.HasSuffix(lowerName, "_z*") { + continue + } + t, en := valueToParamType(child) + fp := flatParam{ + name: strings.ToLower(child.Name), + t: t | protocol.CommandArgValid, + en: en, + } + newPrefix := append(append([]flatParam{}, prefix...), fp) + if len(child.Children) > 0 { + childBranches := flattenBranches(newPrefix, child.Children) + branches = append(branches, childBranches...) + } else { + branches = append(branches, newPrefix) + } + } + return branches +} + +// formatAvailableCommands builds the AvailableCommands packet from the command definitions. +// It creates one overload per branch in the command tree. +func formatAvailableCommands(commands map[string]IEngineResponseCommand) packet.AvailableCommands { + pk := &packet.AvailableCommands{} + var enums []commandEnum + enumIndices := make(map[string]uint32) + var dynamicEnums []commandEnum + dynamicEnumIndices := make(map[string]uint32) + + for alias, c := range commands { + if c.Name != alias { + continue + } + branches := flattenBranches([]flatParam{}, c.Children) + var overloads []protocol.CommandOverload + for _, branch := range branches { + overload := protocol.CommandOverload{} + for _, fp := range branch { + t := fp.t + if len(fp.en.Options) > 0 || fp.en.Type != "" { + key := strings.ToLower(fp.en.Type) + if !fp.en.Dynamic { + index, ok := enumIndices[key] + if !ok { + index = uint32(len(enums)) + enumIndices[key] = index + enums = append(enums, commandEnum{ + Type: key, + Options: lowerOptions(fp.en.Options), + Dynamic: fp.en.Dynamic, + }) + } + t |= protocol.CommandArgEnum | (index << enumIndexShift) + } else { + index, ok := dynamicEnumIndices[key] + if !ok { + index = uint32(len(dynamicEnums)) + dynamicEnumIndices[key] = index + dynamicEnums = append(dynamicEnums, commandEnum{ + Type: key, + Options: lowerOptions(fp.en.Options), + Dynamic: fp.en.Dynamic, + }) + } + t |= protocol.CommandArgSoftEnum | (index << enumIndexShift) + } + } + overload.Parameters = append(overload.Parameters, protocol.CommandParameter{ + Name: fp.name, + Type: t, + Optional: false, + Options: 0, + }) + } + overloads = append(overloads, overload) + } + pk.Commands = append(pk.Commands, protocol.Command{ + Name: strings.ToLower(c.Name), + Description: c.Description, + AliasesOffset: uint32(math.MaxUint32), + Overloads: overloads, + }) + } + pk.DynamicEnums = make([]protocol.DynamicEnum, 0, len(dynamicEnums)) + for _, e := range dynamicEnums { + pk.DynamicEnums = append(pk.DynamicEnums, protocol.DynamicEnum{Type: strings.ToLower(e.Type), Values: e.Options}) + } + enumValueIndices := make(map[string]uint32, len(enums)*3) + pk.EnumValues = make([]string, 0, len(enumValueIndices)) + pk.Enums = make([]protocol.CommandEnum, 0, len(enums)) + for _, enum := range enums { + protoEnum := protocol.CommandEnum{Type: enum.Type} + for _, opt := range enum.Options { + lOpt := strings.ToLower(opt) + index, ok := enumValueIndices[lOpt] + if !ok { + index = uint32(len(pk.EnumValues)) + enumValueIndices[lOpt] = index + pk.EnumValues = append(pk.EnumValues, lOpt) + } + protoEnum.ValueIndices = append(protoEnum.ValueIndices, uint(index)) + } + pk.Enums = append(pk.Enums, protoEnum) + } + return *pk +} + +type CustomCommandRegisterHandler struct { +} + +func (CustomCommandRegisterHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.Text) + if dataPacket.TextType != packet.TextTypeObject { + return true, dataPacket, nil + } + var messageData IMinecraftTextMessage + if err := json.Unmarshal([]byte(dataPacket.Message), &messageData); err != nil { + log.Logger.Error("Failed to parse message JSON", "error", err) + return false, dataPacket, nil + } + message := messageData.RawText[0].Text + if !strings.HasPrefix(message, "[PROXY_SYSTEM][COMMANDS]=") { + return true, dataPacket, nil + } + commandsRaw := strings.TrimPrefix(message, "[PROXY_SYSTEM][COMMANDS]=") + var commands map[string]IEngineResponseCommand + if err := json.Unmarshal([]byte(commandsRaw), &commands); err != nil { + log.Logger.Info("Failed to unmarshal commands", "error", err) + return false, dataPacket, err + } + availableCommands := formatAvailableCommands(commands) + bdsSentCommands := proxy.ProxyInstance.Worlds.BDSAvailableCommands + availableCommands = command.MergeAvailableCommands(availableCommands, bdsSentCommands) + player.DataPacket(&availableCommands) + return false, dataPacket, nil +} diff --git a/custom_handlers/entityNameTagHandler.go b/custom_handlers/entityNameTagHandler.go new file mode 100644 index 0000000..d7fba8b --- /dev/null +++ b/custom_handlers/entityNameTagHandler.go @@ -0,0 +1,188 @@ +package custom_handlers + +import ( + "fmt" + "strings" + + "github.com/HyPE-Network/vanilla-proxy/log" + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/lang" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +// cachedLangMap is a map of cached language translations. +var cachedLangMap map[string]map[string]string = make(map[string]map[string]string) + +// getLanguageToTranslate returns the language to translate based on the players language code. +func getLanguageToTranslate(player human.Human) (string, error) { + pokebedrockResourcePack := proxy.ProxyInstance.ResourcePacks[0] + var languageToTranslate string = "en_US" + + // Read supported languages + supportedLanguages, err := lang.GetSupportedLanguages(pokebedrockResourcePack) + if err != nil { + return languageToTranslate, fmt.Errorf("error while getting supported languages: %w", err) + } + + // Check if players language is supported, if not default to `en_US` + playersDesiredLanguage := player.GetSession().ClientData.LanguageCode + for _, language := range supportedLanguages { + if language == playersDesiredLanguage { + languageToTranslate = playersDesiredLanguage + break + } + } + + return languageToTranslate, nil +} + +// getEntityTranslatedName returns the translated name of a entity type id based on the players language code. +func getEntityTranslatedName(player human.Human, entityTypeId string) (string, error) { + pokebedrockResourcePack := proxy.ProxyInstance.ResourcePacks[0] + // Get the language to translate + languageToTranslate, err := getLanguageToTranslate(player) + if err != nil { + return "", fmt.Errorf("error while getting language to translate: %w", err) + } + + // Check if the translation is already cached, if so return it + if langMap, ok := cachedLangMap[languageToTranslate]; ok { + return langMap["item.spawn_egg.entity."+entityTypeId+".name"], nil + } + + // Get Lang file + langMap, err := lang.GetLangTranslationMap(pokebedrockResourcePack, languageToTranslate) + if err != nil { + return "", fmt.Errorf("error while getting lang translation map: %w", err) + } + + // Get the pokemon name + translatedName := langMap["item.spawn_egg.entity."+entityTypeId+".name"] + if translatedName == "" { + return entityTypeId, fmt.Errorf("could not find translation for entity type id: %s", entityTypeId) + } + + // Cache the translation + cachedLangMap[languageToTranslate] = langMap + + return translatedName, nil +} + +// getTranslatedNameTagOfSentOutPokemon returns the translated name tag of a sent out pokemon. +func getTranslatedNameTagOfSentOutPokemon(player human.Human, entityTypeId string, currentName string) (string, error) { + if strings.HasPrefix(currentName, "§l§n§r") { + // Name is nickname, do not translate + return currentName, nil + } + + // Get the translated name of the pokemon + translatedName, err := getEntityTranslatedName(player, entityTypeId) + if err != nil { + return currentName, fmt.Errorf("error while getting translated name: %w", err) + } + + // Read the current name, and replace the name with the translated name + // Example: "§lImpidimp §eLvl 25\nOwner not found§r" + // Example: "§lCharmander §eLvl 100\nSmell of curry§r" + // Example: "§lTaillow\n§eLvl 21§r" + // Example: "§lIron Treads\n§eLvl 21§r" + // Example: "§lMr. Mime §eLvl 100\nSmell of curry§r" + // Structure: "§l §eLvl <\d>\n§r" + // Structure: "§l\n§eLvl <\d>§r" + + // Sanitize and process the current name string + currentName = strings.TrimSpace(currentName) + lines := strings.Split(currentName, "\n") + + if len(lines) > 0 { + // Extract the level part to correctly replace the name + if strings.Contains(lines[0], "§eLvl") { + parts := strings.Split(lines[0], "§eLvl") + lines[0] = "§l" + translatedName + " §eLvl" + parts[1] + } else { + lines[0] = "§l" + translatedName + } + + // Rejoin the lines into a single string + currentName = strings.Join(lines, "\n") + } else { + currentName = "§l" + translatedName + } + + return currentName, nil +} + +type AddActorNameTagHandler struct{} + +func (h *AddActorNameTagHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.AddActor) + + // Check if the actor is a pokemon + if !strings.HasPrefix(dataPacket.EntityType, "pokemon:") { + return true, pk, nil + } + + // Get the current name of the pokemon + currentName, ok := dataPacket.EntityMetadata[protocol.EntityDataKeyName].(string) + if !ok { + log.Logger.Warn("Could not assert the current name of the pokemon", "metadata", dataPacket.EntityMetadata[protocol.EntityDataKeyName]) + return true, pk, nil + } + + // Get the translated name tag of the sent out pokemon + translatedName, err := getTranslatedNameTagOfSentOutPokemon(player, dataPacket.EntityType, currentName) + if err != nil { + log.Logger.Warn("Could not get the translated name tag of the sent out pokemon", "error", err) + return true, pk, nil + } + + // Update the name in the packet + dataPacket.EntityMetadata[protocol.EntityDataKeyName] = translatedName + + return true, pk, nil +} + +type SetActorDataNameTagHandler struct{} + +func (h *SetActorDataNameTagHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.SetActorData) + + // Get the current name of the pokemon + currentName, ok := dataPacket.EntityMetadata[protocol.EntityDataKeyName].(string) + if !ok { + // NameTag is not being changed in this set data packet, ignore. + return true, pk, nil + } + + // Get the entity type id of the actor, to ensure its a pokemon + entityID, ok := proxy.ProxyInstance.Entities.GetEntityFromRuntimeID(dataPacket.EntityRuntimeID) + if !ok { + // Entity was never being tracked. + return true, pk, nil + } + + actorTypeId, ok := proxy.ProxyInstance.Entities.GetEntityTypeID(entityID) + if !ok { + // AddActor was never called for this entity, so we don't know what type it is. + return true, pk, nil + } + + // Check if the actor is a pokemon + if !strings.HasPrefix(actorTypeId, "pokemon:") { + return true, pk, nil + } + + // Get the translated name tag of the sent out pokemon + translatedName, err := getTranslatedNameTagOfSentOutPokemon(player, actorTypeId, currentName) + if err != nil { + log.Logger.Warn("Could not get the translated name tag of the sent out pokemon", "error", err) + return true, pk, nil + } + + // Update the name in the packet + dataPacket.EntityMetadata[protocol.EntityDataKeyName] = translatedName + + return true, pk, nil +} diff --git a/custom_handlers/inventoryContentHandler.go b/custom_handlers/inventoryContentHandler.go new file mode 100644 index 0000000..feaa208 --- /dev/null +++ b/custom_handlers/inventoryContentHandler.go @@ -0,0 +1,43 @@ +package custom_handlers + +import ( + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type ItemStackRequestHandler struct{} + +func (ItemStackRequestHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.ItemStackRequest) + + for _, request := range dataPacket.Requests { + for _, action := range request.Actions { + //log.Println("Action:", action) + switch td := action.(type) { + case *protocol.PlaceStackRequestAction: + //log.Println("PlaceStackRequestAction") + destId := td.Destination.Container.ContainerID + if destId == protocol.ContainerCraftingInput || destId == protocol.ContainerCombinedHotBarAndInventory { + //log.Println("Item Being set to Crafting Table") + // Most likely setting to a container, log the container ID + copiedDestination := td.Destination + copiedDestination.StackNetworkID = td.Source.StackNetworkID + player.SetItemToContainerSlot(copiedDestination) + } + case *protocol.TakeStackRequestAction: + // log.Println("TakeStackRequestAction") + srcId := td.Source.Container.ContainerID + if srcId == protocol.ContainerCraftingInput || srcId == protocol.ContainerCombinedHotBarAndInventory { + // log.Println("Item Being taken from Crafting Table") + copiedSource := td.Source + copiedSource.StackNetworkID = 0 // Clear the item from the crafting table + player.SetItemToContainerSlot(copiedSource) + } + } + } + player.SetLastItemStackRequestID(request.RequestID) + } + + return true, dataPacket, nil +} diff --git a/custom_handlers/itemComponentHandler.go b/custom_handlers/itemComponentHandler.go new file mode 100644 index 0000000..dcf1024 --- /dev/null +++ b/custom_handlers/itemComponentHandler.go @@ -0,0 +1,18 @@ +package custom_handlers + +import ( + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type ItemComponentHandler struct { +} + +func (ItemComponentHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.ItemRegistry) + + proxy.ProxyInstance.Worlds.SetItemComponentEntries(dataPacket.Items) + + return true, dataPacket, nil +} diff --git a/custom_handlers/openContainer.go b/custom_handlers/openContainer.go new file mode 100644 index 0000000..2edd48b --- /dev/null +++ b/custom_handlers/openContainer.go @@ -0,0 +1,31 @@ +package custom_handlers + +import ( + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type OpenContainerHandler struct{} + +func (OpenContainerHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.ContainerOpen) + + //log.Println("Player has opened a container with ID: ", dataPacket.WindowID, dataPacket.ContainerType) + player.SetOpenContainerWindowID(dataPacket.WindowID) + player.SetOpenContainerType(dataPacket.ContainerType) + + return true, dataPacket, nil +} + +type ContainerCloseHandler struct{} + +func (ContainerCloseHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.ContainerClose) + + //log.Println("Player has closed a container", dataPacket) + player.SetOpenContainerWindowID(0) + player.SetOpenContainerType(0) + player.ClearItemsInContainers() // Clear bc container is closed + + return true, dataPacket, nil +} diff --git a/custom_handlers/playerListHandler.go b/custom_handlers/playerListHandler.go new file mode 100644 index 0000000..9b28529 --- /dev/null +++ b/custom_handlers/playerListHandler.go @@ -0,0 +1,26 @@ +package custom_handlers + +import ( + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type PlayerListHandler struct { +} + +func (PlayerListHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.PlayerList) + + // Map the Entries to add the XUID to the playerlist + for i, entry := range dataPacket.Entries { + xuid, err := proxy.ProxyInstance.PlayerListManager.GetXUIDFromName(entry.Username) + if err != nil { + continue + } + entry.XUID = xuid + dataPacket.Entries[i] = entry + } + + return true, dataPacket, nil +} diff --git a/custom_handlers/signEditHandler.go b/custom_handlers/signEditHandler.go new file mode 100644 index 0000000..3388341 --- /dev/null +++ b/custom_handlers/signEditHandler.go @@ -0,0 +1,17 @@ +package custom_handlers + +import ( + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type SignEditHandler struct { +} + +func (SignEditHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.BlockActorData) + + //log.Logger.Debugln("Update Block: ", dataPacket.Position, dataPacket.NBTData) + + return true, dataPacket, nil +} diff --git a/go.mod b/go.mod index 33d957d..9a0d693 100644 --- a/go.mod +++ b/go.mod @@ -1,33 +1,29 @@ module github.com/HyPE-Network/vanilla-proxy -go 1.21.0 +go 1.24.0 require ( - github.com/DEBANMC/valve-rcon v1.0.5 - github.com/df-mc/atomic v1.10.0 - github.com/go-gl/mathgl v1.1.0 - github.com/google/uuid v1.3.1 + github.com/getsentry/sentry-go v0.31.1 + github.com/go-gl/mathgl v1.2.0 + github.com/gofrs/flock v0.12.1 + github.com/google/uuid v1.6.0 github.com/pelletier/go-toml v1.9.5 - github.com/sandertv/gophertunnel v1.31.0 - github.com/sirupsen/logrus v1.9.3 - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 + github.com/sandertv/go-raknet v1.14.3-0.20250305181847-6af3e95113d6 + github.com/sandertv/gophertunnel v1.45.0 + github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33 ) require ( - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/klauspost/compress v1.15.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/muhammadmuzzammil1998/jsonc v1.0.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/sandertv/go-raknet v1.12.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/image v0.5.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/oauth2 v0.4.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.7.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect - gopkg.in/square/go-jose.v2 v2.6.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect ) + +replace github.com/sandertv/gophertunnel => github.com/smell-of-curry/gophertunnel v1.46.1-0.20250704012025-8e86404b4050 + +replace github.com/sandertv/go-raknet => github.com/smell-of-curry/go-raknet v0.0.0-20250525005230-991ee492a907 diff --git a/go.sum b/go.sum index fb34255..a55854c 100644 --- a/go.sum +++ b/go.sum @@ -1,95 +1,50 @@ -github.com/DEBANMC/valve-rcon v1.0.5 h1:wAdoVOIIxjLU0iFVS4zkZBJc6lyTUQF0sVzXv3fZPqY= -github.com/DEBANMC/valve-rcon v1.0.5/go.mod h1:/Ji5V3bOd0qNI2cCpk38vj1vbQCXHiDs7XMn4yNna2o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/df-mc/atomic v1.10.0 h1:0ZuxBKwR/hxcFGorKiHIp+hY7hgY+XBTzhCYD2NqSEg= -github.com/df-mc/atomic v1.10.0/go.mod h1:Gw9rf+rPIbydMjA329Jn4yjd/O2c/qusw3iNp4tFGSc= -github.com/go-gl/mathgl v1.1.0 h1:0lzZ+rntPX3/oGrDzYGdowSLC2ky8Osirvf5uAwfIEA= -github.com/go-gl/mathgl v1.1.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= -github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/getsentry/sentry-go v0.31.1 h1:ELVc0h7gwyhnXHDouXkhqTFSO5oslsRDk0++eyE0KJ4= +github.com/getsentry/sentry-go v0.31.1/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY= +github.com/go-gl/mathgl v1.2.0/go.mod h1:pf9+b5J3LFP7iZ4XXaVzZrCle0Q/vNpB/vDe5+3ulRE= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/muhammadmuzzammil1998/jsonc v1.0.0 h1:8o5gBQn4ZA3NBA9DlTujCj2a4w0tqWrPVjDwhzkgTIs= github.com/muhammadmuzzammil1998/jsonc v1.0.0/go.mod h1:saF2fIVw4banK0H4+/EuqfFLpRnoy5S+ECwTOCcRcSU= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sandertv/go-raknet v1.12.0 h1:olUzZlIJyX/pgj/mrsLCZYjKLNDsYiWdvQ4NIm3z0DA= -github.com/sandertv/go-raknet v1.12.0/go.mod h1:Gx+WgZBMQ0V2UoouGoJ8Wj6CDrMBQ4SB2F/ggpl5/+Y= -github.com/sandertv/gophertunnel v1.31.0 h1:og9PmD8tXc0CgRJD2N/Yc3KQgYGU1XvAlOeMhfJXUDs= -github.com/sandertv/gophertunnel v1.31.0/go.mod h1:ekREo7U9TPHh86kbuPMaWA93NMyWsfVvP/iNT3XhAb8= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= -golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/smell-of-curry/go-raknet v0.0.0-20250525005230-991ee492a907 h1:5nLl2TlAo5SxtHLCBVKgIdngDG0FWrB1k+nZ578AQ2c= +github.com/smell-of-curry/go-raknet v0.0.0-20250525005230-991ee492a907/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU= +github.com/smell-of-curry/gophertunnel v1.46.1-0.20250704012025-8e86404b4050 h1:fMP0juqij29MKrZqo+ga2JdYbDAqX3OlJEaLXsXjeJg= +github.com/smell-of-curry/gophertunnel v1.46.1-0.20250704012025-8e86404b4050/go.mod h1:lmRarAmn25V/+QeiUbUDXeA26bEaNlX1wGEM/rj39ew= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33 h1:idh63uw+gsG05HwjZsAENCG4KZfyvjK03bpjxa5qRRk= +github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler/handlers/commands_handler.go b/handler/handlers/commands_handler.go deleted file mode 100644 index 87ec5d9..0000000 --- a/handler/handlers/commands_handler.go +++ /dev/null @@ -1,97 +0,0 @@ -package handlers - -import ( - "strings" - - "github.com/HyPE-Network/vanilla-proxy/log" - "github.com/HyPE-Network/vanilla-proxy/proxy" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" - "github.com/HyPE-Network/vanilla-proxy/utils/color" - - "github.com/sandertv/gophertunnel/minecraft/protocol" - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -type AvailableCommandsHandler struct { -} - -type CommandRequestHandler struct { -} - -func (AvailableCommandsHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { - dataPacket := pk.(*packet.AvailableCommands) - - for executor, command := range proxy.ProxyInstance.CommandManager.Commands { - if executor.ForPlayer() || proxy.ProxyInstance.CommandManager.IsOp(player.GetName()) { - dataPacket.Commands = append(dataPacket.Commands, command) - } - } - - // todo: hack - helpCommand := protocol.Command{ - Name: "help", - Description: "Help info", - PermissionLevel: protocol.CommandEnumConstraintCheatsEnabled, - Overloads: []protocol.CommandOverload{}, - } - - dataPacket.Commands = append(dataPacket.Commands, helpCommand) - - return true, pk, nil -} - -func (CommandRequestHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { - dataPacket := pk.(*packet.CommandRequest) - - var command = strings.ToLower(strings.Split(dataPacket.CommandLine[1:], " ")[0]) - - for executor, cmd := range proxy.ProxyInstance.CommandManager.Commands { - if cmd.Name == command { - args := formatArgs(dataPacket.CommandLine[1:]) - - err := executor.Execute(player, args) - if err != nil { - player.SendMessage(color.Red + "Error in command handler!") - log.Logger.Errorln(err) - return true, pk, nil - } - - return false, pk, nil - } - } - - return true, pk, nil -} - -func formatArgs(command string) []string { - var args []string - command = strings.TrimSpace(command) - command += " " - - arg := "" - bigArg := false - for _, value := range command { - if value == ' ' && !bigArg { - if arg != "" && arg != " " { - args = append(args, arg) - } - arg = "" - } else if value == '"' && !bigArg { - bigArg = true - } else if value == '"' && bigArg { - bigArg = false - if arg != "" && arg != " " { - args = append(args, arg) - } - arg = "" - } else { - arg += string(value) - } - } - - if len(args) > 1 { - return args[1:] - } - - return []string{} -} diff --git a/handler/handlers/entities.go b/handler/handlers/entities.go new file mode 100644 index 0000000..94d4b1d --- /dev/null +++ b/handler/handlers/entities.go @@ -0,0 +1,31 @@ +package handlers + +import ( + "github.com/HyPE-Network/vanilla-proxy/proxy" + "github.com/HyPE-Network/vanilla-proxy/proxy/entity" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +type AddActorHandler struct{} + +func (ah AddActorHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.AddActor) + + proxy.ProxyInstance.Entities.SetEntity(dataPacket.EntityUniqueID, entity.EntityData{ + TypeID: dataPacket.EntityType, + RuntimeID: dataPacket.EntityRuntimeID, + }) + + return true, pk, nil +} + +type RemoveActorHandler struct{} + +func (ah RemoveActorHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { + dataPacket := pk.(*packet.RemoveActor) + + proxy.ProxyInstance.Entities.RemoveEntity(dataPacket.EntityUniqueID) + + return true, pk, nil +} diff --git a/handler/handlers/fake_inventory_handler.go b/handler/handlers/fake_inventory_handler.go deleted file mode 100644 index ba7e058..0000000 --- a/handler/handlers/fake_inventory_handler.go +++ /dev/null @@ -1,52 +0,0 @@ -package handlers - -import ( - "github.com/HyPE-Network/vanilla-proxy/proxy" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" - - "github.com/sandertv/gophertunnel/minecraft/protocol" - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -type CloseInventoryHandler struct { -} - -func (CloseInventoryHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { - dataPacket := pk.(*packet.ContainerClose) - - if dataPacket.WindowID == player.GetData().Windows && player.GetData().FakeChestOpen { - player.GetData().FakeChestOpen = false - player.SendAirUpdate(player.GetData().FakeChestPos) - player.GetData().Windows-- - player.GetData().FakeChestPos = protocol.BlockPos{} - player.DataPacket(dataPacket) - return false, pk, nil - } - - return true, pk, nil -} - -type OpenInventoryHandler struct { -} - -type OpenInventoryHandlerBoarder struct { -} - -func (OpenInventoryHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { - dataPacket := pk.(*packet.ContainerOpen) - - player.GetData().Windows = dataPacket.WindowID - - return true, pk, nil -} - -func (OpenInventoryHandlerBoarder) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { - dataPacket := pk.(*packet.ContainerOpen) - - isInside := proxy.ProxyInstance.Worlds.Border.IsPositionInside([]int32{dataPacket.ContainerPosition.X(), dataPacket.ContainerPosition.Y(), dataPacket.ContainerPosition.Z()}) - if !isInside { - return false, pk, nil - } - - return true, pk, nil -} diff --git a/handler/handlers/handlers.go b/handler/handlers/handlers.go index 35aa347..c340b06 100644 --- a/handler/handlers/handlers.go +++ b/handler/handlers/handlers.go @@ -1,6 +1,9 @@ package handlers import ( + "fmt" + "slices" + "github.com/HyPE-Network/vanilla-proxy/handler" "github.com/HyPE-Network/vanilla-proxy/log" "github.com/HyPE-Network/vanilla-proxy/proxy" @@ -24,7 +27,7 @@ var ignored = []uint32{ packet.IDCraftingData, packet.IDBiomeDefinitionList, packet.IDPlayerList, - packet.IDItemComponent, + packet.IDItemRegistry, packet.IDLevelEvent, packet.IDSetActorMotion, packet.IDUpdateAttributes, @@ -51,24 +54,23 @@ func registerHandlers() map[uint32][]handler.PacketHandler { handlers[packet.IDSubChunkRequest] = []handler.PacketHandler{SubChunkRequestHandler{}} handlers[packet.IDSubChunk] = append(handlers[packet.IDSubChunk], SubChunkHandlerBoarder{}) handlers[packet.IDLevelChunk] = []handler.PacketHandler{LevelChunkHandler{}} - handlers[packet.IDContainerOpen] = []handler.PacketHandler{OpenInventoryHandlerBoarder{}} handlers[packet.IDInventoryTransaction] = []handler.PacketHandler{InventoryTransactionHandler{}} } - handlers[packet.IDModalFormResponse] = []handler.PacketHandler{ModalFormResponseHandler{}} + // handlers[packet.IDModalFormResponse] = []handler.PacketHandler{ModalFormResponseHandler{}} handlers[packet.IDPlayerAuthInput] = []handler.PacketHandler{PlayerInputHandler{}} handlers[packet.IDChunkRadiusUpdated] = []handler.PacketHandler{UpdateRadiusHandler{proxy.ProxyInstance.Config.Server.ViewDistance}} handlers[packet.IDRequestChunkRadius] = []handler.PacketHandler{RequestRadiusHandler{proxy.ProxyInstance.Config.Server.ViewDistance}} - handlers[packet.IDContainerClose] = []handler.PacketHandler{CloseInventoryHandler{}} - handlers[packet.IDContainerOpen] = []handler.PacketHandler{OpenInventoryHandler{}} - - handlers[packet.IDCommandRequest] = []handler.PacketHandler{CommandRequestHandler{}} - handlers[packet.IDAvailableCommands] = []handler.PacketHandler{AvailableCommandsHandler{}} + // handlers[packet.IDCommandRequest] = []handler.PacketHandler{CommandRequestHandler{}} + // handlers[packet.IDAvailableCommands] = []handler.PacketHandler{AvailableCommandsHandler{}} handlers[packet.IDPacketViolationWarning] = []handler.PacketHandler{MalformedHandler{}} + handlers[packet.IDAddActor] = []handler.PacketHandler{AddActorHandler{}} + handlers[packet.IDRemoveActor] = []handler.PacketHandler{RemoveActorHandler{}} + return handlers } @@ -90,6 +92,10 @@ func (hm handlerManager) HandlePacket(pk packet.Packet, player human.Human, send sendDebug(pk, sender) } + if hm.PacketHandlers == nil { + return false, pk, fmt.Errorf("packet handlers map is nil") + } + packetHandlers, hasHandler := hm.PacketHandlers[pk.ID()] if hasHandler { for _, packetHandler := range packetHandlers { @@ -98,6 +104,9 @@ func (hm handlerManager) HandlePacket(pk packet.Packet, player human.Human, send } else { _, pk, err = packetHandler.Handle(pk, player) } + if err != nil { + return false, pk, err + } } } @@ -107,27 +116,18 @@ func (hm handlerManager) HandlePacket(pk packet.Packet, player human.Human, send func sendDebug(pk packet.Packet, sender string) { switch debug { case debugLevelAll: - log.Logger.Debugln(sender, ":", pk.ID(), ">", pk) + log.Logger.Debug("Packet debug", "sender", sender, "id", pk.ID(), "packet", pk) case debugLevelNotIgnored: - if !contains(ignored, pk.ID()) { - log.Logger.Debugln(sender, ":", pk.ID(), ">", pk) + if !slices.Contains(ignored, pk.ID()) { + log.Logger.Debug("Packet debug", "sender", sender, "id", pk.ID(), "packet", pk) } case debugLevelTarget: - if contains(target, pk.ID()) { - log.Logger.Debugln(sender, ":", pk.ID(), ">", pk) - } - } -} - -func contains(a []uint32, x uint32) bool { - for _, n := range a { - if x == n { - return true + if slices.Contains(target, pk.ID()) { + log.Logger.Debug("Packet debug", "sender", sender, "id", pk.ID(), "packet", pk) } } - return false } const ( diff --git a/handler/handlers/inventory_transaction_handler.go b/handler/handlers/inventory_transaction_handler.go index 8868075..e48ef96 100644 --- a/handler/handlers/inventory_transaction_handler.go +++ b/handler/handlers/inventory_transaction_handler.go @@ -2,7 +2,6 @@ package handlers import ( "github.com/HyPE-Network/vanilla-proxy/proxy" - "github.com/HyPE-Network/vanilla-proxy/proxy/block/cube" "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" "github.com/sandertv/gophertunnel/minecraft/protocol" @@ -18,9 +17,7 @@ func (InventoryTransactionHandler) Handle(pk packet.Packet, player human.Human) switch td := dataPacket.TransactionData.(type) { case *protocol.UseItemTransactionData: if td.ActionType == protocol.UseItemActionClickBlock { - pos := cube.Side(td.BlockPosition, td.BlockFace) - - if !proxy.ProxyInstance.Worlds.Border.IsXZInside(pos.X(), pos.Z()) { + if !proxy.ProxyInstance.Worlds.Border.IsXZInside(td.BlockPosition.X(), td.BlockPosition.Z()) { player.SendMessage("§cActions outside the world are prohibited!") return false, pk, nil } diff --git a/handler/handlers/malformed_handler.go b/handler/handlers/malformed_handler.go index 8fa7ac7..3417a61 100644 --- a/handler/handlers/malformed_handler.go +++ b/handler/handlers/malformed_handler.go @@ -13,7 +13,7 @@ type MalformedHandler struct { func (MalformedHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { dataPacket := pk.(*packet.PacketViolationWarning) - log.Logger.Errorln(player.GetName(), "> Malformed", dataPacket) + log.Logger.Error("Malformed packet", "player", player.GetName(), "packet", dataPacket) return true, pk, nil } diff --git a/handler/handlers/modal_form_response_handler.go b/handler/handlers/modal_form_response_handler.go deleted file mode 100644 index 332310e..0000000 --- a/handler/handlers/modal_form_response_handler.go +++ /dev/null @@ -1,30 +0,0 @@ -package handlers - -import ( - "github.com/HyPE-Network/vanilla-proxy/log" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/form" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" - - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -type ModalFormResponseHandler struct { -} - -func (ModalFormResponseHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { - dataPacket := pk.(*packet.ModalFormResponse) - - window, ok := player.GetData().Forms[dataPacket.FormID] - if ok { - closed, formData, err := form.GetResponseData(dataPacket, window.GetType()) - if err != nil { - log.Logger.Errorln(err) - return false, pk, nil - } - - window.Do(closed, formData) - delete(player.GetData().Forms, dataPacket.FormID) - } - - return false, pk, nil -} diff --git a/handler/handlers/player_input_handler.go b/handler/handlers/player_input_handler.go index 61d0ce8..d80fa02 100644 --- a/handler/handlers/player_input_handler.go +++ b/handler/handlers/player_input_handler.go @@ -13,6 +13,35 @@ type PlayerInputHandler struct { func (PlayerInputHandler) Handle(pk packet.Packet, player human.Human) (bool, packet.Packet, error) { dataPacket := pk.(*packet.PlayerAuthInput) + playerData := player.GetData().GameData + + player.SetPlayerLocation(dataPacket.Position) + + // Verify new position + // if proxy.ProxyInstance.Worlds != nil && !proxy.ProxyInstance.Worlds.Border.IsXZInside(int32(dataPacket.Position.X()), int32(dataPacket.Position.Z())) { + // player.SendMessage("§cYou cannot move outside the world!") + // player.PlaySound("note.bass", playerData.PlayerPosition, 1, 1) + // movePlayerPk := &packet.MovePlayer{ + // EntityRuntimeID: playerData.EntityRuntimeID, + // Position: playerData.PlayerPosition, + // Pitch: playerData.Pitch, + // Yaw: playerData.Yaw, + // HeadYaw: playerData.Yaw, + // OnGround: true, + // Mode: packet.MoveModeTeleport, + // TeleportCause: packet.TeleportCauseCommand, + // Tick: dataPacket.ClientTick, + // } + // player.DataPacket(movePlayerPk) + + // // If player is in a vehicle, when they get teleported back they will stay in vehicle, so we need to + // // tell the server/client to kick the player out of the vehicle. + // jumpPacket := &packet.PassengerJump{} + // player.DataPacketToServer(jumpPacket) + + // return false, pk, nil + // } + player.GetData().GameData.Pitch, player.GetData().GameData.Yaw = dataPacket.Pitch, dataPacket.Yaw player.GetData().GameData.PlayerPosition = dataPacket.Position @@ -33,6 +62,7 @@ func (PlayerInputHandler) Handle(pk packet.Packet, player human.Human) (bool, pa if proxy.ProxyInstance.Worlds != nil && !proxy.ProxyInstance.Worlds.Border.IsXZInside(bPos.X(), bPos.Z()) { player.SendMessage("§cActions outside the world are prohibited!") + player.PlaySound("note.bass", playerData.PlayerPosition, 1, 1) return false, pk, nil } } diff --git a/log/log.go b/log/log.go index b395b4a..132a936 100644 --- a/log/log.go +++ b/log/log.go @@ -1,17 +1,19 @@ package log -import "github.com/sirupsen/logrus" +import ( + "log/slog" + "os" +) -var Logger *logrus.Logger +var Logger *slog.Logger -func New() *logrus.Logger { - log := logrus.New() - log.Formatter = &logrus.TextFormatter{ - ForceColors: true, - TimestampFormat: "2006-01-02 15:04:05", - FullTimestamp: true, +func New() *slog.Logger { + opts := &slog.HandlerOptions{ + Level: slog.LevelDebug, } - log.Level = logrus.DebugLevel - return log + handler := slog.NewTextHandler(os.Stdout, opts) + logger := slog.New(handler) + + return logger } diff --git a/main.go b/main.go index af2ae0c..aa743f3 100644 --- a/main.go +++ b/main.go @@ -1,24 +1,104 @@ package main import ( + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/HyPE-Network/vanilla-proxy/custom_handlers" + "github.com/HyPE-Network/vanilla-proxy/handler" "github.com/HyPE-Network/vanilla-proxy/handler/handlers" "github.com/HyPE-Network/vanilla-proxy/log" "github.com/HyPE-Network/vanilla-proxy/proxy" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/manager" "github.com/HyPE-Network/vanilla-proxy/utils" + "github.com/getsentry/sentry-go" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + + _ "net/http/pprof" ) func main() { log.Logger = log.New() - log.Logger.Debugln("Logger has been started") + log.Logger.Debug("Logger has been started!") + // Load configuration config := utils.ReadConfig() - proxy.ProxyInstance = proxy.New(config, manager.NewPlayerManager()) - - err := proxy.ProxyInstance.Start(handlers.New()) - if err != nil { - log.Logger.Errorln("Error while starting server: ", err) - panic(err) + // Initialize Sentry only if DSN is provided + if config.Logging.SentryDsn != "" { + err := sentry.Init(sentry.ClientOptions{ + Dsn: config.Logging.SentryDsn, + ServerName: config.Server.Prefix, // Use the server prefix as the server name in Sentry + }) + if err != nil { + log.Logger.Error("sentry.Init", "error", err) + panic(err) // panic if Sentry fails to initialize + } + // Flush buffered events before the program terminates. + defer sentry.Flush(2 * time.Second) + } else { + log.Logger.Info("Sentry DSN not provided, error tracking is disabled") } + + go func() { + err := http.ListenAndServe(config.Logging.ProfilerHost, nil) + if err != nil { + log.Logger.Error("Failed to start pprof server", "error", err) + } + }() + + proxy.ProxyInstance = proxy.New(config) + + // Create a channel to catch shutdown signals + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + // Start the handlers + handlerManager := loadHandlers() + + // Start the proxy in a goroutine + go func() { + err := proxy.ProxyInstance.Start(handlerManager) + if err != nil { + log.Logger.Error("Error while starting server", "error", err) + } + }() + + // Wait for shutdown signal + <-sigCh + + // Perform graceful shutdown + proxy.ProxyInstance.Shutdown() +} + +func loadHandlers() handler.HandlerManager { + // Store the repeating task so it can be stopped if needed + claimTask := utils.NewRepeatingTask(60, func() { + custom_handlers.FetchClaims() + }) + + // Register the task for cleanup + proxy.ProxyInstance.RegisterCleanupTask(func() { + claimTask.Stop() + }) + + h := handlers.New() + h.RegisterHandler(packet.IDAvailableCommands, custom_handlers.AvailableCommandsHandler{}) + h.RegisterHandler(packet.IDCommandRequest, custom_handlers.CommandRequestHandler{}) + h.RegisterHandler(packet.IDBlockActorData, custom_handlers.SignEditHandler{}) + h.RegisterHandler(packet.IDInventoryTransaction, custom_handlers.ClaimInventoryTransactionHandler{}) + h.RegisterHandler(packet.IDPlayerAuthInput, custom_handlers.ClaimPlayerAuthInputHandler{}) + h.RegisterHandler(packet.IDText, custom_handlers.CustomCommandRegisterHandler{}) + h.RegisterHandler(packet.IDText, custom_handlers.ChatLoggingHandler{}) + h.RegisterHandler(packet.IDItemRegistry, custom_handlers.ItemComponentHandler{}) + h.RegisterHandler(packet.IDContainerOpen, custom_handlers.OpenContainerHandler{}) + h.RegisterHandler(packet.IDContainerClose, custom_handlers.ContainerCloseHandler{}) + h.RegisterHandler(packet.IDItemStackRequest, custom_handlers.ItemStackRequestHandler{}) + h.RegisterHandler(packet.IDPlayerList, custom_handlers.PlayerListHandler{}) + h.RegisterHandler(packet.IDAddActor, &custom_handlers.AddActorNameTagHandler{}) + h.RegisterHandler(packet.IDSetActorData, &custom_handlers.SetActorDataNameTagHandler{}) + + return h } diff --git a/proxy/block/cube/cube.go b/proxy/block/cube/cube.go deleted file mode 100644 index 50bd494..0000000 --- a/proxy/block/cube/cube.go +++ /dev/null @@ -1,36 +0,0 @@ -package cube - -import "github.com/sandertv/gophertunnel/minecraft/protocol" - -const ( - // FaceDown represents the bottom face of a block. - FaceDown = iota - // FaceUp represents the top face of a block. - FaceUp - // FaceNorth represents the north face of a block. - FaceNorth - // FaceSouth represents the south face of a block. - FaceSouth - // FaceWest represents the west face of the block. - FaceWest - // FaceEast represents the east face of the block. - FaceEast -) - -func Side(p protocol.BlockPos, face int32) protocol.BlockPos { - switch face { - case FaceUp: - p[1]++ - case FaceDown: - p[1]-- - case FaceNorth: - p[2]-- - case FaceSouth: - p[2]++ - case FaceWest: - p[0]-- - case FaceEast: - p[0]++ - } - return p -} diff --git a/proxy/block/definitions.go b/proxy/block/definitions.go deleted file mode 100644 index f433793..0000000 --- a/proxy/block/definitions.go +++ /dev/null @@ -1,5 +0,0 @@ -package block - -const ( - Chest = "minecraft:chest" -) diff --git a/proxy/command/command.go b/proxy/command/command.go deleted file mode 100644 index 85589e3..0000000 --- a/proxy/command/command.go +++ /dev/null @@ -1,40 +0,0 @@ -package command - -import ( - "strings" - - "github.com/HyPE-Network/vanilla-proxy/proxy/command/sender" - - "github.com/sandertv/gophertunnel/minecraft/protocol" -) - -type CommandManager struct { - Ops []string - Commands map[CommandExecutor]protocol.Command -} - -type CommandExecutor interface { - Execute(sender.CommandSender, []string) error - ForPlayer() bool -} - -func InitManager(ops []string) *CommandManager { - return &CommandManager{ - Ops: ops, - Commands: make(map[CommandExecutor]protocol.Command), - } -} - -func (cm *CommandManager) RegisterCommand(command protocol.Command, executor CommandExecutor) { - cm.Commands[executor] = command -} - -func (cm *CommandManager) IsOp(name string) bool { - for _, player_name := range cm.Ops { - if strings.EqualFold(player_name, name) { - return true - } - } - - return false -} diff --git a/proxy/command/rcon.go b/proxy/command/rcon.go deleted file mode 100644 index f74b121..0000000 --- a/proxy/command/rcon.go +++ /dev/null @@ -1,48 +0,0 @@ -package command - -import ( - "strings" - - "github.com/HyPE-Network/vanilla-proxy/log" - - rcon "github.com/DEBANMC/valve-rcon" - "github.com/sandertv/gophertunnel/minecraft/protocol" -) - -type RconSender struct { - Client rcon.Client -} - -func (rs *RconSender) SendMessage(mess string) { - rs.Client.Write(mess) -} - -func InitRCON(commands map[CommandExecutor]protocol.Command, port int, pass string) { - rconSrv := rcon.NewRCON("0.0.0.0", port, pass) - rconSrv.SetBanList([]string{}) - - rconSrv.OnCommand(func(commandMessage string, client rcon.Client) { - sender := &RconSender{ - Client: client, - } - - for executor, command := range commands { - if command.Name == commandMessage { - split := strings.Fields(commandMessage) - err := executor.Execute(sender, append(split[:0], split[1:]...)) - if err != nil { - sender.SendMessage(err.Error()) - } - } - } - }) - - log.Logger.Println("Rcon-server has been started") - - err := rconSrv.ListenAndServe() - if err != nil { - panic(err) - } - - rconSrv.CloseOnProgramEnd() -} diff --git a/proxy/command/sender/sender.go b/proxy/command/sender/sender.go deleted file mode 100644 index 51766ee..0000000 --- a/proxy/command/sender/sender.go +++ /dev/null @@ -1,5 +0,0 @@ -package sender - -type CommandSender interface { - SendMessage(message string) -} diff --git a/proxy/command/utils.go b/proxy/command/utils.go new file mode 100644 index 0000000..fc4b3b4 --- /dev/null +++ b/proxy/command/utils.go @@ -0,0 +1,286 @@ +package command + +import ( + "github.com/HyPE-Network/vanilla-proxy/log" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +// MergeAvailableCommands merges two AvailableCommands packets into one. +func MergeAvailableCommands(pk1, pk2 packet.AvailableCommands) packet.AvailableCommands { + mergedPacket := packet.AvailableCommands{} + + // Merge and deduplicate EnumValues, returns a map for index adjustments + EnumValues, pk1EnumValuesMap, pk2EnumValuesMap := mergeUniqueStrings(pk1.EnumValues, pk2.EnumValues) + mergedPacket.EnumValues = EnumValues + + // Merge and deduplicate ChainedSubcommandValues + ChainedSubcommandValues, pk1ChainedMap, pk2ChainedMap := mergeUniqueStrings(pk1.ChainedSubcommandValues, pk2.ChainedSubcommandValues) + mergedPacket.ChainedSubcommandValues = ChainedSubcommandValues + + // Merge Enums + Enums, pk1EnumMap, pk2EnumMap := mergeUniqueEnums(pk1.Enums, pk2.Enums, pk1EnumValuesMap, pk2EnumValuesMap) + mergedPacket.Enums = Enums + + // Merge and deduplicate Suffixes + Suffixes, _, _ := mergeUniqueStrings(pk1.Suffixes, pk2.Suffixes) + mergedPacket.Suffixes = Suffixes + + // Merge ChainedSubcommands + mergedPacket.ChainedSubcommands = mergeChainedSubcommands(pk1.ChainedSubcommands, pk2.ChainedSubcommands, pk1ChainedMap, pk2ChainedMap) + + // Merge DynamicEnums + DynamicEnums, _, _ := mergeUniqueDynamicEnumsWithIndexMaps(pk1.DynamicEnums, pk2.DynamicEnums) + mergedPacket.DynamicEnums = DynamicEnums + + // Merge Commands + mergedPacket.Commands = mergeUniqueCommands(pk1.Commands, pk2.Commands, pk1ChainedMap, pk2ChainedMap, pk1EnumMap, pk2EnumMap) + + // Merge Constraints + mergedPacket.Constraints = mergeUniqueConstraints(pk1.Constraints, pk2.Constraints, pk1EnumValuesMap, pk2EnumValuesMap) + + return mergedPacket +} + +// Helper function to merge and deduplicate strings and return a map for index adjustments +func mergeUniqueStrings(slice1, slice2 []string) ([]string, map[uint]uint, map[uint]uint) { + uniqueMap := make(map[string]uint) + indexMap1 := make(map[uint]uint) + indexMap2 := make(map[uint]uint) + uniqueSlice := make([]string, 0) + + for i, item := range slice1 { + if idx, exists := uniqueMap[item]; exists { + // Already exists, map previous index to this. + indexMap1[uint(i)] = idx + } else { + // Not found, add to uniqueSlice and map new index. + newIdx := uint(len(uniqueSlice)) + uniqueMap[item] = newIdx + indexMap1[uint(i)] = newIdx + uniqueSlice = append(uniqueSlice, item) + } + } + + for i, item := range slice2 { + if idx, exists := uniqueMap[item]; exists { + indexMap2[uint(i)] = idx + } else { + newIdx := uint(len(uniqueSlice)) + uniqueMap[item] = newIdx + indexMap2[uint(i)] = newIdx + uniqueSlice = append(uniqueSlice, item) + } + } + + return uniqueSlice, indexMap1, indexMap2 +} + +// Helper function to merge and deduplicate CommandEnum slices, will also pass index maps for EnumValues +func mergeUniqueEnums(slice1, slice2 []protocol.CommandEnum, enumValuesIndexMap1, enumValuesIndexMap2 map[uint]uint) ([]protocol.CommandEnum, map[uint]uint, map[uint]uint) { + uniqueMap := make(map[string]uint) + indexMap1 := make(map[uint]uint) + indexMap2 := make(map[uint]uint) + uniqueSlice := make([]protocol.CommandEnum, 0) + + for i, item := range slice1 { + item = updateEnumIndices(item, enumValuesIndexMap1) + if idx, exists := uniqueMap[item.Type]; exists { + // Already exists, map previous index to this. + indexMap1[uint(i)] = idx + // Merge the current enum, with this enum + uniqueSlice[idx] = mergeEnums(uniqueSlice[idx], item) + } else { + // Not found, add to uniqueSlice and map new index. + newIdx := uint(len(uniqueSlice)) + uniqueMap[item.Type] = newIdx + indexMap1[uint(i)] = newIdx + uniqueSlice = append(uniqueSlice, item) + } + } + + for i, item := range slice2 { + item = updateEnumIndices(item, enumValuesIndexMap2) + if idx, exists := uniqueMap[item.Type]; exists { + indexMap2[uint(i)] = idx + // Merge the current enum, with this enum + uniqueSlice[idx] = mergeEnums(uniqueSlice[idx], item) + } else { + newIdx := uint(len(uniqueSlice)) + uniqueMap[item.Type] = newIdx + indexMap2[uint(i)] = newIdx + uniqueSlice = append(uniqueSlice, item) + } + } + + return uniqueSlice, indexMap1, indexMap2 +} + +// Helper function to merge two CommandEnums +func mergeEnums(enum1, enum2 protocol.CommandEnum) protocol.CommandEnum { + uniqueIndices := make(map[uint]struct{}) + for _, idx := range enum1.ValueIndices { + uniqueIndices[idx] = struct{}{} + } + for _, idx := range enum2.ValueIndices { + uniqueIndices[idx] = struct{}{} + } + mergedIndices := make([]uint, 0, len(uniqueIndices)) + for idx := range uniqueIndices { + mergedIndices = append(mergedIndices, idx) + } + enum1.ValueIndices = mergedIndices + return enum1 +} + +// Helper function to update CommandEnum indices +func updateEnumIndices(enum protocol.CommandEnum, indexMap map[uint]uint) protocol.CommandEnum { + updatedEnum := enum + updatedEnum.ValueIndices = make([]uint, len(enum.ValueIndices)) + for i, idx := range enum.ValueIndices { + updatedEnum.ValueIndices[i] = indexMap[idx] + } + return updatedEnum +} + +// Helper function to merge ChainedSubcommand slices +func mergeChainedSubcommands(slice1, slice2 []protocol.ChainedSubcommand, indexMap1, indexMap2 map[uint]uint) []protocol.ChainedSubcommand { + mergedSlice := make([]protocol.ChainedSubcommand, 0) + for _, item := range slice1 { + mergedSlice = append(mergedSlice, updateChainedSubcommandIndices(item, indexMap1)) + } + for _, item := range slice2 { + mergedSlice = append(mergedSlice, updateChainedSubcommandIndices(item, indexMap2)) + } + return mergedSlice +} + +// Helper function to update ChainedSubcommand indices +func updateChainedSubcommandIndices(chained protocol.ChainedSubcommand, indexMap map[uint]uint) protocol.ChainedSubcommand { + updatedChained := chained + updatedChained.Values = make([]protocol.ChainedSubcommandValue, len(chained.Values)) + for i, val := range chained.Values { + val.Index = uint16(indexMap[uint(val.Index)]) + updatedChained.Values[i] = val + } + return updatedChained +} + +// Helper function to merge and deduplicate Command slices +func mergeUniqueCommands(slice1, slice2 []protocol.Command, chainedCommandsIndexMap1, chainedCommandsIndexMap2, enumsIndexMap1, enumsIndexMap2 map[uint]uint) []protocol.Command { + uniqueMap := make(map[string]protocol.Command) + for _, item := range slice1 { + uniqueMap[item.Name] = updateCommandIndices(item, chainedCommandsIndexMap1, enumsIndexMap1) + } + for _, item := range slice2 { + if _, exists := uniqueMap[item.Name]; exists { + continue // Skip duplicates + } else { + uniqueMap[item.Name] = updateCommandIndices(item, chainedCommandsIndexMap2, enumsIndexMap2) + } + } + uniqueSlice := make([]protocol.Command, 0, len(uniqueMap)) + for _, item := range uniqueMap { + uniqueSlice = append(uniqueSlice, item) + } + return uniqueSlice +} + +// Helper function to update Command indices +func updateCommandIndices(cmd protocol.Command, chainedCommandsIndexMap, enumsIndexMap map[uint]uint) protocol.Command { + updatedCmd := cmd + updatedCmd.ChainedSubcommandOffsets = make([]uint16, len(cmd.ChainedSubcommandOffsets)) + for i, offset := range cmd.ChainedSubcommandOffsets { + updatedCmd.ChainedSubcommandOffsets[i] = uint16(chainedCommandsIndexMap[uint(offset)]) + } + if updatedCmd.AliasesOffset != ^uint32(0) { + updatedCmd.AliasesOffset = uint32(enumsIndexMap[uint(cmd.AliasesOffset)]) + } + return updatedCmd +} + +// Helper function to merge and deduplicate DynamicEnum slices and return index maps +func mergeUniqueDynamicEnumsWithIndexMaps(slice1, slice2 []protocol.DynamicEnum) ([]protocol.DynamicEnum, map[uint]uint, map[uint]uint) { + uniqueMap := make(map[string]uint) + indexMap1 := make(map[uint]uint) + indexMap2 := make(map[uint]uint) + uniqueSlice := make([]protocol.DynamicEnum, 0) + + for i, item := range slice1 { + if idx, exists := uniqueMap[item.Type]; exists { + // Already exists, merge the values and map previous index to this. + log.Logger.Info("Duplicate dynamic enum type in slice1", "type", item.Type) + indexMap1[uint(i)] = idx + mergedValues, _, _ := mergeUniqueStrings(uniqueSlice[idx].Values, item.Values) + uniqueSlice[idx].Values = mergedValues + } else { + // Not found, add to uniqueSlice and map new index. + newIdx := uint(len(uniqueSlice)) + uniqueMap[item.Type] = newIdx + indexMap1[uint(i)] = newIdx + uniqueSlice = append(uniqueSlice, item) + } + } + + for i, item := range slice2 { + if idx, exists := uniqueMap[item.Type]; exists { + log.Logger.Info("Duplicate dynamic enum type in slice2", "type", item.Type) + indexMap2[uint(i)] = idx + // Merge the current dynamic enum values with this one. + mergedValues, _, _ := mergeUniqueStrings(uniqueSlice[idx].Values, item.Values) + uniqueSlice[idx].Values = mergedValues + } else { + newIdx := uint(len(uniqueSlice)) + uniqueMap[item.Type] = newIdx + indexMap2[uint(i)] = newIdx + uniqueSlice = append(uniqueSlice, item) + } + } + + return uniqueSlice, indexMap1, indexMap2 +} + +// Helper function to merge and deduplicate CommandEnumConstraint slices +func mergeUniqueConstraints(slice1, slice2 []protocol.CommandEnumConstraint, indexMap1, indexMap2 map[uint]uint) []protocol.CommandEnumConstraint { + uniqueMap := make(map[uint32]protocol.CommandEnumConstraint) + for _, item := range slice1 { + uniqueMap[item.EnumIndex] = updateConstraintIndices(item, indexMap1) + } + for _, item := range slice2 { + if existing, exists := uniqueMap[item.EnumIndex]; exists { + uniqueMap[item.EnumIndex] = mergeConstraints(existing, updateConstraintIndices(item, indexMap2)) + } else { + uniqueMap[item.EnumIndex] = updateConstraintIndices(item, indexMap2) + } + } + uniqueSlice := make([]protocol.CommandEnumConstraint, 0, len(uniqueMap)) + for _, item := range uniqueMap { + uniqueSlice = append(uniqueSlice, item) + } + return uniqueSlice +} + +// Helper function to merge two CommandEnumConstraints +func mergeConstraints(constraint1, constraint2 protocol.CommandEnumConstraint) protocol.CommandEnumConstraint { + uniqueConstraints := make(map[byte]struct{}) + for _, c := range constraint1.Constraints { + uniqueConstraints[c] = struct{}{} + } + for _, c := range constraint2.Constraints { + uniqueConstraints[c] = struct{}{} + } + mergedConstraints := make([]byte, 0, len(uniqueConstraints)) + for c := range uniqueConstraints { + mergedConstraints = append(mergedConstraints, c) + } + constraint1.Constraints = mergedConstraints + return constraint1 +} + +// Helper function to update CommandEnumConstraint indices +func updateConstraintIndices(constraint protocol.CommandEnumConstraint, indexMap map[uint]uint) protocol.CommandEnumConstraint { + updatedConstraint := constraint + updatedConstraint.EnumValueIndex = uint32(indexMap[uint(constraint.EnumValueIndex)]) + updatedConstraint.EnumIndex = uint32(indexMap[uint(constraint.EnumIndex)]) + return updatedConstraint +} diff --git a/proxy/console/bash/bash.go b/proxy/console/bash/bash.go deleted file mode 100644 index 09eeebf..0000000 --- a/proxy/console/bash/bash.go +++ /dev/null @@ -1,28 +0,0 @@ -package bash - -import "os/exec" - -type Bash struct { - Screen string -} - -func NewBash(screen string) *Bash { - return &Bash{ - Screen: screen, - } -} - -func (bash *Bash) SendCommand(command string) error { - bashCommand := bash.CommandBuilder(command) - cmd := exec.Command("bash", "-c", bashCommand) - err := cmd.Start() - - return err -} - -func (bash *Bash) CommandBuilder(command string) string { - bashCommand := "screen -S " + bash.Screen + " -X eval 'stuff \"" + command + "\"\\015'" - return bashCommand -} - -func (bash *Bash) Close() {} diff --git a/proxy/console/bots/bot.go b/proxy/console/bots/bot.go deleted file mode 100644 index 8f749c3..0000000 --- a/proxy/console/bots/bot.go +++ /dev/null @@ -1,105 +0,0 @@ -package bots - -import ( - "github.com/HyPE-Network/vanilla-proxy/log" - "github.com/HyPE-Network/vanilla-proxy/utils" - - "github.com/google/uuid" - "github.com/sandertv/gophertunnel/minecraft" - "github.com/sandertv/gophertunnel/minecraft/protocol" - "github.com/sandertv/gophertunnel/minecraft/protocol/login" - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -type Bot struct { - Address string - XUID string - DisplayName string - Conn *minecraft.Conn - Connected bool -} - -func NewBot(config utils.Config) *Bot { - bot := &Bot{ - Address: config.Connection.RemoteAddress, - XUID: config.Bot.XUID, - DisplayName: config.Bot.DisplayName, - } - - if err := bot.spawn(); err != nil { - log.Logger.Errorln("Bot:", err) - return nil - } - - return bot -} - -func (bot *Bot) spawn() error { - serverConn, err := minecraft.Dialer{ - KeepXBLIdentityData: true, - IdentityData: login.IdentityData{ - XUID: bot.XUID, - DisplayName: bot.DisplayName, - }, - }.Dial("raknet", bot.Address) - if err != nil { - return err - } - - bot.Conn = serverConn - - if err := serverConn.DoSpawn(); err != nil { - return err - } - - log.Logger.Debugln("Bot spawned with username", bot.DisplayName) - - bot.Connected = true - bot.SendCommand("gamemode c") - bot.SendCommand("tp 10000 200 10000") - - go func() { - for { - _, err := serverConn.ReadPacket() - if err != nil { - break - } - } - - bot.Close() - }() - - return nil -} - -func (bot *Bot) SendCommand(command string) error { - if bot.Connected { - cpk := &packet.CommandRequest{ - CommandLine: "/" + command, - CommandOrigin: protocol.CommandOrigin{ - Origin: 0, - UUID: uuid.New(), - RequestID: "", - PlayerUniqueID: 0, - }, - Internal: false, - } - - return bot.Conn.WritePacket(cpk) - } else { - if err := bot.spawn(); err != nil { - log.Logger.Errorln("Bot:", err) - return err - } - - return bot.SendCommand(command) - } -} - -func (bot *Bot) Close() { - if err := bot.Conn.Close(); err != nil { - log.Logger.Errorln("Bot:", err) - } - log.Logger.Debugln("Bot despawned") - bot.Connected = false -} diff --git a/proxy/console/console.go b/proxy/console/console.go deleted file mode 100644 index 3966604..0000000 --- a/proxy/console/console.go +++ /dev/null @@ -1,6 +0,0 @@ -package console - -type CommandSender interface { - SendCommand(string) error - Close() -} diff --git a/proxy/entity/entity.go b/proxy/entity/entity.go new file mode 100644 index 0000000..439126d --- /dev/null +++ b/proxy/entity/entity.go @@ -0,0 +1,82 @@ +package entity + +import "sync" + +type EntityData struct { + TypeID string + RuntimeID uint64 +} + +type Entities struct { + IdToData map[int64]EntityData + mu sync.RWMutex +} + +// Init initializes the entities package. +func Init() *Entities { + return &Entities{ + IdToData: make(map[int64]EntityData), + } +} + +// SetEntity sets the entity type of an entity with the specified runtime ID. +func (entities *Entities) SetEntity(actorId int64, data EntityData) { + entities.mu.Lock() + defer entities.mu.Unlock() + + entities.IdToData[actorId] = data +} + +// GetEntity returns the entity type of an entity with the specified runtime ID. +func (entities *Entities) GetEntity(actorId int64) (EntityData, bool) { + entities.mu.RLock() + defer entities.mu.RUnlock() + + entityData, ok := entities.IdToData[actorId] + return entityData, ok +} + +// GetEntityTypeID returns the entity type ID of the entity with the specified entity ID. +func (entities *Entities) GetEntityTypeID(actorId int64) (string, bool) { + entities.mu.RLock() + defer entities.mu.RUnlock() + + entityData, ok := entities.IdToData[actorId] + if !ok { + return "", ok + } + return entityData.TypeID, ok +} + +// GetEntityRuntimeID returns the runtime ID of the entity with the specified entity ID. +func (entities *Entities) GetEntityRuntimeID(actorId int64) (uint64, bool) { + entities.mu.RLock() + defer entities.mu.RUnlock() + + entityData, ok := entities.IdToData[actorId] + if !ok { + return 0, ok + } + return entityData.RuntimeID, ok +} + +// GetEntityFromRuntimeID returns the entity ID of the entity with the specified runtime ID. +func (entities *Entities) GetEntityFromRuntimeID(runtimeID uint64) (int64, bool) { + entities.mu.RLock() + defer entities.mu.RUnlock() + + for actorID, entityData := range entities.IdToData { + if entityData.RuntimeID == runtimeID { + return actorID, true + } + } + return 0, false +} + +// RemoveEntity removes the entity with the specified runtime ID. +func (entities *Entities) RemoveEntity(actorId int64) { + entities.mu.Lock() + defer entities.mu.Unlock() + + delete(entities.IdToData, actorId) +} diff --git a/proxy/inventory/chest/chest.go b/proxy/inventory/chest/chest.go deleted file mode 100644 index 4f92991..0000000 --- a/proxy/inventory/chest/chest.go +++ /dev/null @@ -1,21 +0,0 @@ -package chest - -import "github.com/sandertv/gophertunnel/minecraft/protocol" - -type Chest struct { - Content []protocol.ItemInstance -} - -func NewChest() *Chest { - return &Chest{ - Content: make([]protocol.ItemInstance, 27), - } -} - -func (chest *Chest) GetContent() []protocol.ItemInstance { - return chest.Content -} - -func (chest *Chest) SetItem(index int, item protocol.ItemInstance) { - chest.Content[index] = item -} diff --git a/proxy/inventory/inventory.go b/proxy/inventory/inventory.go deleted file mode 100644 index 3f34f9a..0000000 --- a/proxy/inventory/inventory.go +++ /dev/null @@ -1,8 +0,0 @@ -package inventory - -import "github.com/sandertv/gophertunnel/minecraft/protocol" - -type Inventory interface { - GetContent() []protocol.ItemInstance - SetItem(int, protocol.ItemInstance) -} diff --git a/proxy/item/definitions.go b/proxy/item/definitions.go deleted file mode 100644 index 4ee52be..0000000 --- a/proxy/item/definitions.go +++ /dev/null @@ -1,6 +0,0 @@ -package item - -const ( - FlintAndSteel = 299 - Stick = 320 -) diff --git a/proxy/lang/lang.go b/proxy/lang/lang.go new file mode 100644 index 0000000..2c423b9 --- /dev/null +++ b/proxy/lang/lang.go @@ -0,0 +1,60 @@ +package lang + +import ( + "bufio" + "encoding/json" + "fmt" + "strings" + + "github.com/HyPE-Network/vanilla-proxy/utils" + "github.com/sandertv/gophertunnel/minecraft/resource" +) + +// GetSupportedLanguages returns a list of supported languages from a resource pack. +func GetSupportedLanguages(pack *resource.Pack) ([]string, error) { + supportedLanguageBytes, err := pack.ReadFile("texts/languages.json") + if err != nil { + return nil, fmt.Errorf("error while reading languages.json: %w", err) + } + var supportedLanguages []string + supportedLanguagesJSON, err := utils.ParseCommentedJSON([]byte(supportedLanguageBytes)) + if err != nil { + return nil, fmt.Errorf("error while parsing languages.json: %w", err) + } + err = json.Unmarshal(supportedLanguagesJSON, &supportedLanguages) + if err != nil { + return nil, fmt.Errorf("error while un-marshalling languages.json: %w", err) + } + return supportedLanguages, nil +} + +// GetLangTranslationMap returns a map of translations for a specific language from a resource pack. +func GetLangTranslationMap(pack *resource.Pack, language string) (map[string]string, error) { + languageBytes, err := pack.ReadFile("texts/" + language + ".lang") + if err != nil { + return nil, fmt.Errorf("error while reading language file: %w", err) + } + + langMap := make(map[string]string) + scanner := bufio.NewScanner(strings.NewReader(string(languageBytes))) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + // Skip empty lines or comments + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + langMap[key] = value + } else { + // Line has more than one `=` sign, or none at all, throw an error + return nil, fmt.Errorf("error while parsing line in .lang file: %s", line) + } + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error while scanning .lang file: %w", err) + } + return langMap, nil +} diff --git a/proxy/player/data/data.go b/proxy/player/data/data.go index 3994c67..31bf924 100644 --- a/proxy/player/data/data.go +++ b/proxy/player/data/data.go @@ -1,30 +1,30 @@ package data import ( - "github.com/HyPE-Network/vanilla-proxy/proxy/player/form" - - "github.com/df-mc/atomic" + "github.com/go-gl/mathgl/mgl32" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol" ) type PlayerData struct { - GameData minecraft.GameData - Forms map[uint32]form.Form - Closed bool - BrokenBlocks map[protocol.BlockPos]uint32 - CurrentScoreboard atomic.Value[string] - CurrentLines atomic.Value[[]string] - StartSessionTime int64 - Authorized bool - FakeChestOpen bool - FakeChestPos protocol.BlockPos - Windows byte -} - -func (pd *PlayerData) SetClosed() { - pd.Closed = true + GameData minecraft.GameData + StartSessionTime int64 + Authorized bool + Windows byte + + // Disconnected is true if the player is currently being disconnected from the server. + Disconnected bool + // OpenContainerWindowId is the ID of the window that is currently open for the player. + OpenContainerWindowId byte + // OpenContainerType is the type of container that is currently open for the player. + OpenContainerType byte + // LastItemStackRequestID is the last ID of an item stack request that was sent by the player. + LastItemStackRequestID int32 + // ItemsInContainers holds a list of all items that are currently in containers the player has put in. + ItemsInContainers []protocol.StackRequestSlotInfo + // LastUpdatedLocation is the last location that was updated for the player (updated by auth-input). + LastUpdatedLocation mgl32.Vec3 } func (pd *PlayerData) GetNextWindowId() byte { diff --git a/proxy/player/form/element.go b/proxy/player/form/element.go deleted file mode 100644 index 069ce84..0000000 --- a/proxy/player/form/element.go +++ /dev/null @@ -1,16 +0,0 @@ -package form - -type Button struct { - // Text holds the text displayed on the button. It may use Minecraft formatting codes and may have - // newlines. - Text string `json:"text"` - // Image holds a path to an image for the button. The Image may either be a URL pointing to an image, - // such as 'https://someimagewebsite.com/someimage.png', or a path pointing to a local asset, such as - // 'textures/block/grass_carried'. - Image any `json:"image"` -} - -type ImageType struct { - Type string `json:"type"` - Data string `json:"data"` -} diff --git a/proxy/player/form/form_api.go b/proxy/player/form/form_api.go deleted file mode 100644 index f71d52e..0000000 --- a/proxy/player/form/form_api.go +++ /dev/null @@ -1,79 +0,0 @@ -package form - -import ( - "bytes" - "encoding/json" - "fmt" - - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -const ( - SimpleType string = "form" - ModalType string = "modal" - CustomType string = "custom_form" - - ImagePath string = "path" - ImageURL string = "url" -) - -type Form interface { - Encode() (packet.Packet, uint32, error) - MarshalJSON() ([]byte, error) - GetType() string - Do(bool, any) -} - -func GetResponseData(response *packet.ModalFormResponse, formType string) (bool, any, error) { - _, closed := response.CancelReason.Value() - if closed { - switch formType { - case SimpleType: - return true, uint(0), nil - case ModalType: - return true, false, nil - case CustomType: - return true, []string{}, nil - } - } - - data, ok := response.ResponseData.Value() - if !ok { - switch formType { - case SimpleType: - return true, 0, nil - case ModalType: - return true, false, nil - case CustomType: - return true, []string{}, nil - } - } - - switch formType { - case SimpleType: - var ind uint - err := json.Unmarshal(data, &ind) - if err != nil { - return false, nil, err - } - return false, ind, nil - case ModalType: - var value bool - if err := json.Unmarshal(data, &value); err != nil { - return false, nil, fmt.Errorf("error parsing JSON as bool: %w", err) - } - return false, value, nil - case CustomType: - dec := json.NewDecoder(bytes.NewBuffer(data)) - dec.UseNumber() - - var data []string - if err := dec.Decode(&data); err != nil { - return false, nil, fmt.Errorf("error decoding JSON data to slice: %w", err) - } - - return false, data, nil - } - - return false, nil, fmt.Errorf("unknown type") -} diff --git a/proxy/player/form/forms/modal/modal.go b/proxy/player/form/forms/modal/modal.go deleted file mode 100644 index 18d59a4..0000000 --- a/proxy/player/form/forms/modal/modal.go +++ /dev/null @@ -1,100 +0,0 @@ -package modal - -import ( - "encoding/json" - "math/rand" - - "github.com/HyPE-Network/vanilla-proxy/log" - "github.com/HyPE-Network/vanilla-proxy/proxy/player" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/form" - "github.com/HyPE-Network/vanilla-proxy/utils" - - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -type ModalForm struct { - title, body string - Buttons [2]string - operation func(bool, bool) -} - -func CreateModalForm(title string, body string) ModalForm { - return ModalForm{ - title: title, - body: body, - Buttons: [2]string{}, - } -} - -func (m ModalForm) Encode() (packet.Packet, uint32, error) { - id := rand.Uint32() - - data, err := m.MarshalJSON() - if err != nil { - return nil, 0, err - } - - pk := &packet.ModalFormRequest{ - FormID: id, - FormData: data, - } - - return pk, id, nil -} - -// MarshalJSON ... -func (m ModalForm) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - "type": form.ModalType, - "title": m.title, - "content": m.body, - "button1": m.Buttons[0], - "button2": m.Buttons[1], - }) -} - -func (m ModalForm) GetType() string { - return form.ModalType -} - -func (m ModalForm) Do(closed bool, data any) { - m.operation(closed, data.(bool)) -} - -func (m *ModalForm) SetFirstButton(text string) { - m.Buttons[0] = text -} - -func (m *ModalForm) SetSecondButton(text string) { - m.Buttons[1] = text -} - -func (m *ModalForm) SetTitle(text string) { - m.title = text -} - -func (m *ModalForm) SetBody(text ...any) { - m.body = utils.Format(text) -} - -func (m *ModalForm) Send(player *player.Player) { - pk, _, err := m.Encode() - if err != nil { - log.Logger.Errorln(err) - return - } - - player.DataPacket(pk) -} - -func (m *ModalForm) SendFunc(player *player.Player, f func(bool, bool)) { - pk, id, err := m.Encode() - if err != nil { - log.Logger.Errorln(err) - return - } - - m.operation = f - player.PlayerData.Forms[id] = m - player.DataPacket(pk) -} diff --git a/proxy/player/form/forms/simple/simple.go b/proxy/player/form/forms/simple/simple.go deleted file mode 100644 index 0df3661..0000000 --- a/proxy/player/form/forms/simple/simple.go +++ /dev/null @@ -1,98 +0,0 @@ -package simple - -import ( - "encoding/json" - "math/rand" - - "github.com/HyPE-Network/vanilla-proxy/log" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/form" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" - "github.com/HyPE-Network/vanilla-proxy/utils" - - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -type SimpleForm struct { - title, body string - Buttons []form.Button - operation func(bool, uint) -} - -func CreateSimpleForm(title string, body string) SimpleForm { - return SimpleForm{ - title: title, - body: body, - Buttons: []form.Button{}, - } -} - -func (m SimpleForm) Encode() (packet.Packet, uint32, error) { - id := rand.Uint32() - - data, err := m.MarshalJSON() - if err != nil { - return nil, 0, err - } - - pk := &packet.ModalFormRequest{ - FormID: id, - FormData: data, - } - - return pk, id, nil -} - -func (m SimpleForm) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]any{ - "type": form.SimpleType, - "title": m.title, - "content": m.body, - "buttons": m.Buttons, - }) -} - -func (m SimpleForm) GetType() string { - return form.SimpleType -} - -func (m SimpleForm) Do(closed bool, data any) { - m.operation(closed, data.(uint)) -} - -func (m *SimpleForm) SetTitle(text string) { - m.title = text -} - -func (m *SimpleForm) SetBody(text ...any) { - m.body = utils.Format(text) -} - -func (m *SimpleForm) AddButton(text string) { - m.Buttons = append(m.Buttons, form.Button{Text: text}) -} - -func (m *SimpleForm) AddImageButton(text string, image form.ImageType) { - m.Buttons = append(m.Buttons, form.Button{Text: text, Image: image}) -} - -func (m *SimpleForm) Send(player human.Human) { - pk, _, err := m.Encode() - if err != nil { - log.Logger.Errorln(err) - return - } - - player.DataPacket(pk) -} - -func (m *SimpleForm) SendFunc(player human.Human, f func(bool, uint)) { - pk, id, err := m.Encode() - if err != nil { - log.Logger.Errorln(err) - return - } - - m.operation = f - player.GetData().Forms[id] = m - player.DataPacket(pk) -} diff --git a/proxy/player/human/human.go b/proxy/player/human/human.go index 5543b93..7157683 100644 --- a/proxy/player/human/human.go +++ b/proxy/player/human/human.go @@ -1,13 +1,10 @@ package human import ( - "github.com/HyPE-Network/vanilla-proxy/proxy/inventory" "github.com/HyPE-Network/vanilla-proxy/proxy/player/data" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/scoreboard" "github.com/HyPE-Network/vanilla-proxy/proxy/session" "github.com/go-gl/mathgl/mgl32" - "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "github.com/sandertv/gophertunnel/minecraft/protocol" @@ -22,10 +19,6 @@ type Human interface { SendPopup(string) SendTip(string) - HasScoreboard() bool - SendScoreboard(*scoreboard.Scoreboard) - RemoveScoreboard() - Transfer(string, uint16) Kick() @@ -37,28 +30,33 @@ type Human interface { SendUpdateBlock(protocol.BlockPos, uint32) SendAirUpdate(protocol.BlockPos) + PlaySound(string, mgl32.Vec3, float32, float32) + InOverworld() bool InNether() bool InEnd() bool GetDimension() int32 GetWorldName() string + SetPlayerLocation(mgl32.Vec3) + GetPing() int64 GetSessionTime() int64 DataPacket(packet.Packet) DataPacketToServer(packet.Packet) - SendInventory(inventory.Inventory) -} + SendXUIDToAddon() + + SetOpenContainerWindowID(windowId byte) + SetOpenContainerType(containerType byte) + SetLastItemStackRequestID(id int32) + GetNextItemStackRequestID() int32 + SetItemToContainerSlot(slotInfo protocol.StackRequestSlotInfo) + ClearItemsInContainers() + GetItemFromContainerSlot(containerID byte, slot byte) protocol.StackRequestSlotInfo + GetCursorItem() protocol.StackRequestSlotInfo -type HumanManager interface { - AddPlayer(*minecraft.Conn, *minecraft.Conn) Human - DeletePlayer(Human) - DeleteAll() - GetPlayer(string) Human - GetPlayerExact(string) Human - PlayerList() map[string]Human - PlayersCount() int - IsOnline(string) bool + IsBeingDisconnected() bool + SetDisconnected(disconnected bool) } diff --git a/proxy/player/manager/player_manager.go b/proxy/player/manager/player_manager.go deleted file mode 100644 index b4b20c4..0000000 --- a/proxy/player/manager/player_manager.go +++ /dev/null @@ -1,94 +0,0 @@ -package manager - -import ( - "math" - "strings" - - "github.com/HyPE-Network/vanilla-proxy/log" - "github.com/HyPE-Network/vanilla-proxy/proxy/player" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" - "github.com/HyPE-Network/vanilla-proxy/proxy/session" - - "github.com/sandertv/gophertunnel/minecraft" -) - -type PlayerManager struct { - Players map[string]human.Human -} - -func NewPlayerManager() *PlayerManager { - return &PlayerManager{ - Players: make(map[string]human.Human), - } -} - -func (pm *PlayerManager) AddPlayer(conn *minecraft.Conn, serverConn *minecraft.Conn) human.Human { - ab := session.NewBridge(conn, serverConn) - newSession := session.NewSession(conn.IdentityData(), conn.ClientData(), ab) - var pl human.Human = player.NewPlayer(conn.IdentityData().DisplayName, newSession, conn.GameData()) - - pm.Players[conn.IdentityData().DisplayName] = pl - - return pl -} - -func (pm *PlayerManager) DeletePlayer(player human.Human) { - if _, ok := pm.Players[player.GetName()]; ok { - delete(pm.Players, player.GetName()) - log.Logger.Infoln(player.GetName(), "left the server") - } - - player.GetSession().Connection.ServerConn.Close() - player.GetSession().Connection.ClientConn.Close() -} - -func (pm *PlayerManager) DeleteAll() { - for _, pl := range pm.Players { - pm.DeletePlayer(pl) - } -} - -func (pm *PlayerManager) GetPlayer(name string) human.Human { - name = strings.ToLower(name) - dt := math.MaxUint8 - var found human.Human - for _, pl := range pm.Players { - if strings.HasPrefix(strings.ToLower(pl.GetName()), name) { - cdt := len(pl.GetName()) - len(name) - if cdt < dt { - found = pl - dt = cdt - } - if cdt == 0 { - break - } - } - } - - return found -} - -func (pm *PlayerManager) GetPlayerExact(name string) human.Human { - name = strings.ToLower(name) - for _, pl := range pm.Players { - if strings.ToLower(pl.GetName()) == name { - return pl - } - } - - return nil -} - -func (pm *PlayerManager) IsOnline(name string) bool { - h := pm.GetPlayerExact(name) - - return h != nil -} - -func (pm *PlayerManager) PlayerList() map[string]human.Human { - return pm.Players -} - -func (pm *PlayerManager) PlayersCount() int { - return len(pm.Players) -} diff --git a/proxy/player/player.go b/proxy/player/player.go index 3393564..4e098dc 100644 --- a/proxy/player/player.go +++ b/proxy/player/player.go @@ -4,12 +4,10 @@ import ( "math" "github.com/HyPE-Network/vanilla-proxy/log" - "github.com/HyPE-Network/vanilla-proxy/proxy/block" - "github.com/HyPE-Network/vanilla-proxy/proxy/inventory" "github.com/HyPE-Network/vanilla-proxy/proxy/player/data" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/form" - "github.com/HyPE-Network/vanilla-proxy/proxy/player/scoreboard" + "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" "github.com/HyPE-Network/vanilla-proxy/proxy/session" + "github.com/HyPE-Network/vanilla-proxy/proxy/world" "github.com/HyPE-Network/vanilla-proxy/utils" "github.com/go-gl/mathgl/mgl32" @@ -24,20 +22,32 @@ type Player struct { PlayerData *data.PlayerData } -func NewPlayer(name string, session *session.Session, gameData minecraft.GameData) *Player { +// Creates a new player instance from a server conn +func NewPlayer(conn *minecraft.Conn, session *session.Session) *Player { + parsedGameData := conn.GameData() + // Remove Items & Blocks from `parsedGameData` to reduce the size of the struct + parsedGameData.Items = nil + parsedGameData.CustomBlocks = nil + return &Player{ - Name: name, + Name: conn.IdentityData().DisplayName, Session: session, PlayerData: &data.PlayerData{ - GameData: gameData, - Forms: make(map[uint32]form.Form), - BrokenBlocks: make(map[protocol.BlockPos]uint32), + GameData: parsedGameData, StartSessionTime: utils.GetTimestamp(), Authorized: false, }, } } +// Gets a player from a connection +func GetPlayer(conn *minecraft.Conn, serverConn *minecraft.Conn) human.Human { + ab := session.NewBridge(conn, serverConn) + newSession := session.NewSession(conn, ab) + var pl human.Human = NewPlayer(conn, newSession) + return pl +} + func (player *Player) GetName() string { return player.Name } @@ -73,58 +83,6 @@ func (player *Player) SendSound(sound string, volume float32, pitch float32) { player.DataPacket(pk) } -func (player *Player) HasScoreboard() bool { - return player.PlayerData.CurrentScoreboard.Load() != "" -} - -func (player *Player) SendScoreboard(sb *scoreboard.Scoreboard) { - currentName, currentLines := player.PlayerData.CurrentScoreboard.Load(), player.PlayerData.CurrentLines.Load() - - if currentName != sb.Name() { - player.RemoveScoreboard() - player.DataPacket(&packet.SetDisplayObjective{ - DisplaySlot: "sidebar", - ObjectiveName: sb.Name(), - DisplayName: sb.Name(), - CriteriaName: "dummy", - }) - player.PlayerData.CurrentScoreboard.Store(sb.Name()) - player.PlayerData.CurrentLines.Store(append([]string(nil), sb.Lines()...)) - } else { - // Remove all current lines from the scoreboard. We can't replace them without removing them. - pk := &packet.SetScore{ActionType: packet.ScoreboardActionRemove} - for i := range currentLines { - pk.Entries = append(pk.Entries, protocol.ScoreboardEntry{ - EntryID: int64(i), - ObjectiveName: currentName, - Score: int32(i), - }) - } - if len(pk.Entries) > 0 { - player.DataPacket(pk) - } - } - pk := &packet.SetScore{ActionType: packet.ScoreboardActionModify} - for k, line := range sb.Lines() { - pk.Entries = append(pk.Entries, protocol.ScoreboardEntry{ - EntryID: int64(k), - ObjectiveName: sb.Name(), - Score: int32(k), - IdentityType: protocol.ScoreboardIdentityFakePlayer, - DisplayName: line, - }) - } - if len(pk.Entries) > 0 { - player.DataPacket(pk) - } -} - -func (player *Player) RemoveScoreboard() { - player.DataPacket(&packet.RemoveObjective{ObjectiveName: player.PlayerData.CurrentScoreboard.Load()}) - player.PlayerData.CurrentScoreboard.Store("") - player.PlayerData.CurrentLines.Store([]string{}) -} - func (player *Player) Transfer(address string, port uint16) { pk := &packet.Transfer{ Address: address, @@ -132,7 +90,7 @@ func (player *Player) Transfer(address string, port uint16) { } player.DataPacket(pk) - log.Logger.Debugln("Player", player.Name, "transferred to", address, port) + log.Logger.Debug("Player transferred", "name", player.Name, "address", address, "port", port) } func (player *Player) Kick() { @@ -169,7 +127,7 @@ func (player *Player) DistanceXYZSquared(x float32, y float32, z float32) float6 } func (player *Player) SendAirUpdate(pos protocol.BlockPos) { - player.SendUpdateBlock(pos, block.AirRID) + player.SendUpdateBlock(pos, world.AirRID) } func (player *Player) SendUpdateBlock(pos protocol.BlockPos, rid uint32) { @@ -183,6 +141,10 @@ func (player *Player) SendUpdateBlock(pos protocol.BlockPos, rid uint32) { player.DataPacket(pk) } +func (player *Player) SetPlayerLocation(pos mgl32.Vec3) { + player.PlayerData.LastUpdatedLocation = pos +} + func (player *Player) InOverworld() bool { return player.PlayerData.GameData.Dimension == packet.DimensionOverworld } @@ -226,55 +188,118 @@ func (player *Player) textPacket(message string, textType byte) { } func (player *Player) DataPacket(pk packet.Packet) { - if player.GetData().Closed { - return - } - if err := player.Session.Connection.ClientConn.WritePacket(pk); err != nil { - log.Logger.Errorln(err) + log.Logger.Error("Failed to write packet to client", "error", err) } } func (player *Player) DataPacketToServer(pk packet.Packet) { if err := player.Session.Connection.ServerConn.WritePacket(pk); err != nil { - log.Logger.Errorln(err) + log.Logger.Error("Failed to write packet to server", "error", err) } } -func (player *Player) SendInventory(inv inventory.Inventory) { - updateBlock := &packet.UpdateBlock{ - Position: protocol.BlockPos{int32(player.PlayerData.GameData.PlayerPosition.X()), int32(player.PlayerData.GameData.PlayerPosition.Y()), int32(player.PlayerData.GameData.PlayerPosition.Z())}, - NewBlockRuntimeID: block.GetRuntime(block.Chest), - Flags: 0, - Layer: 0, +func (player *Player) PlaySound(soundName string, pos mgl32.Vec3, volume float32, pitch float32) { + pk := &packet.PlaySound{ + SoundName: soundName, + Position: pos, + Volume: volume, + Pitch: pitch, } - if err := player.Session.Connection.ClientConn.WritePacket(updateBlock); err != nil { - log.Logger.Errorln(err) - } + player.DataPacket(pk) +} - inventoryId := player.PlayerData.GetNextWindowId() +func (player *Player) SendXUIDToAddon() { + playerXuid := player.GetSession().IdentityData.XUID + message := "XUID=" + playerXuid + " | NAME=" + player.GetName() - openInventory := &packet.ContainerOpen{ - WindowID: inventoryId, - ContainerType: 0, - ContainerPosition: protocol.BlockPos{int32(player.PlayerData.GameData.PlayerPosition.X()), int32(player.PlayerData.GameData.PlayerPosition.Y()), int32(player.PlayerData.GameData.PlayerPosition.Z())}, - ContainerEntityUniqueID: -1, + // Encrypt Message with database key + config := utils.ReadConfig() + encryptedMessage, err := utils.EncryptMessage(message, config.Encryption.Key) + if err != nil { + log.Logger.Error("Failed to encrypt XUID message", "error", err) + return } - if err := player.Session.Connection.ClientConn.WritePacket(openInventory); err != nil { - log.Logger.Errorln(err) + playerXuidTextPacket := &packet.Text{ + TextType: packet.TextTypeChat, + NeedsTranslation: false, + SourceName: player.GetName(), + Message: "[PROXY_XUID] " + encryptedMessage, + Parameters: nil, + XUID: playerXuid, + PlatformChatID: "", } + player.DataPacketToServer(playerXuidTextPacket) +} - player.PlayerData.FakeChestOpen = true - player.PlayerData.FakeChestPos = protocol.BlockPos{int32(player.PlayerData.GameData.PlayerPosition.X()), int32(player.PlayerData.GameData.PlayerPosition.Y()), int32(player.PlayerData.GameData.PlayerPosition.Z())} +// SetOpenContainerWindowID sets the ID of the window that is currently open for the player. +func (player *Player) SetOpenContainerWindowID(windowId byte) { + player.PlayerData.OpenContainerWindowId = windowId +} + +func (player *Player) SetOpenContainerType(containerType byte) { + player.PlayerData.OpenContainerType = containerType +} - inventoryContent := &packet.InventoryContent{ - WindowID: uint32(inventoryId), - Content: inv.GetContent(), +// SetLastItemStackRequestID sets the last item stack request ID that was sent by the player. +func (player *Player) SetLastItemStackRequestID(id int32) { + player.PlayerData.LastItemStackRequestID = id +} + +// GetNextItemStackRequestID returns the next item stack request ID that can be used by the player. +func (player *Player) GetNextItemStackRequestID() int32 { + if player.PlayerData.LastItemStackRequestID == math.MaxInt32 { + player.PlayerData.LastItemStackRequestID = 0 } + player.PlayerData.LastItemStackRequestID -= 2 + return player.PlayerData.LastItemStackRequestID +} - if err := player.Session.Connection.ClientConn.WritePacket(inventoryContent); err != nil { - log.Logger.Errorln(err) +// SetItemToContainerSlot sets the amount of items that are in the container slot that the player has put in. +func (player *Player) SetItemToContainerSlot(slotInfo protocol.StackRequestSlotInfo) { + // Find if the slot is already in this list, if so update, else append + for i, slot := range player.PlayerData.ItemsInContainers { + if slot.Container.ContainerID == slotInfo.Container.ContainerID && slot.Slot == slotInfo.Slot { + player.PlayerData.ItemsInContainers[i] = slotInfo + return + } } + player.PlayerData.ItemsInContainers = append(player.PlayerData.ItemsInContainers, slotInfo) +} + +func (player *Player) ClearItemsInContainers() { + player.PlayerData.ItemsInContainers = nil +} + +// GetItemsInContainerSlot returns the amount of items that are in the container slot that the player has put in. +func (player *Player) GetItemFromContainerSlot(containerID byte, slot byte) protocol.StackRequestSlotInfo { + for _, slotInfo := range player.PlayerData.ItemsInContainers { + if slotInfo.Container.ContainerID == containerID && slotInfo.Slot == slot { + return slotInfo + } + } + return protocol.StackRequestSlotInfo{ + Container: protocol.FullContainerName{ + ContainerID: containerID, + }, + Slot: slot, + StackNetworkID: 0, // Empty + } +} + +// GetCursorItem returns the item that is currently in the cursor of the player. +func (player *Player) GetCursorItem() protocol.StackRequestSlotInfo { + return player.GetItemFromContainerSlot(protocol.ContainerCursor, 0) +} + +// IsBeingDisconnected returns true if the player is currently being disconnected from the server. +func (player *Player) IsBeingDisconnected() bool { + return player.PlayerData.Disconnected +} + +// SetDisconnected sets the disconnected state of the player. If true, the player is currently being disconnected +func (player *Player) SetDisconnected(disconnected bool) { + player.PlayerData.Disconnected = disconnected } diff --git a/proxy/player/scoreboard/scoreboard.go b/proxy/player/scoreboard/scoreboard.go deleted file mode 100644 index 102e98e..0000000 --- a/proxy/player/scoreboard/scoreboard.go +++ /dev/null @@ -1,94 +0,0 @@ -package scoreboard - -import ( - "fmt" - "strings" - - "github.com/HyPE-Network/vanilla-proxy/log" - - "golang.org/x/exp/slices" -) - -// Scoreboard represents a scoreboard that may be sent to a player. The scoreboard is shown on the right side -// of the player's screen. -// Scoreboard implements the io.Writer and io.StringWriter interfaces. fmt.Fprintf and fmt.Fprint may be used -// to write formatted text to the scoreboard. -type Scoreboard struct { - name string - lines []string - padding bool -} - -// New returns a new scoreboard with the display name passed. Once returned, lines may be added to the -// scoreboard to add text to it. The name is formatted according to the rules of fmt.Sprintln. -// Changing the scoreboard after sending it to a player will not update the scoreboard of the player -// automatically: Player.SendScoreboard() must be called again to update it. -func New(name ...any) *Scoreboard { - return &Scoreboard{name: strings.TrimSuffix(fmt.Sprintln(name...), "\n"), padding: true} -} - -// Name returns the display name of the scoreboard, as passed during the construction of the scoreboard. -func (board *Scoreboard) Name() string { - return board.name -} - -// Write writes a slice of data as text to the scoreboard. Newlines may be written to create a new line on -// the scoreboard. -func (board *Scoreboard) Write(p []byte) int { - return board.WriteString(string(p)) -} - -// WriteString writes a string of text to the scoreboard. Newlines may be written to create a new line on -// the scoreboard. -func (board *Scoreboard) WriteString(s string) int { - lines := strings.Split(s, "\n") - board.lines = append(board.lines, lines...) - - // Scoreboards can have up to 15 lines. (16 including the title.) - if len(board.lines) >= 15 { - log.Logger.Errorln("write scoreboard: maximum of 15 lines of text exceeded") - return len(lines) - } - return len(lines) -} - -// Set changes a specific line in the scoreboard and adds empty lines until this index is reached. Set panics if the -// index passed is negative or 15+. -func (board *Scoreboard) Set(index int, s string) { - if index < 0 || index >= 15 { - panic(fmt.Sprintf("index out of range %v", index)) - } - if diff := index - (len(board.lines) - 1); diff > 0 { - board.lines = append(board.lines, make([]string, diff)...) - } - // Remove new lines from the string - board.lines[index] = strings.TrimSuffix(strings.TrimSuffix(s, "\n"), "\n") -} - -// Remove removes a specific line from the scoreboard. Remove panics if the index passed is negative or 15+. -func (board *Scoreboard) Remove(index int) { - if index < 0 || index >= 15 { - panic(fmt.Sprintf("index out of range %v", index)) - } - board.lines = append(board.lines[:index], board.lines[index+1:]...) -} - -// RemovePadding removes the padding of one space that is added to the start of every line. -func (board *Scoreboard) RemovePadding() { - board.padding = false -} - -// Lines returns the data of the Scoreboard as a slice of strings. -func (board *Scoreboard) Lines() []string { - lines := slices.Clone(board.lines) - if board.padding { - for i, line := range lines { - if len(board.name)-len(line)-2 <= 0 { - lines[i] = " " + line + " " - continue - } - lines[i] = " " + line + strings.Repeat(" ", len(board.name)-len(line)-2) - } - } - return lines -} diff --git a/proxy/playerlist/playerlist.go b/proxy/playerlist/playerlist.go new file mode 100644 index 0000000..7cade3e --- /dev/null +++ b/proxy/playerlist/playerlist.go @@ -0,0 +1,212 @@ +package playerlist + +import ( + "encoding/json" + "errors" + "os" + "sync" + + "github.com/HyPE-Network/vanilla-proxy/log" + "github.com/gofrs/flock" + "github.com/sandertv/gophertunnel/minecraft" + "github.com/sandertv/gophertunnel/minecraft/protocol/login" +) + +type Player struct { + PlayerName string `json:"playerName"` + Identity string `json:"identity"` + ClientSelfSignedID string `json:"clientSelfSignedID"` +} + +type PlayerlistManager struct { + mu sync.Mutex + Players map[string]Player `json:"players"` +} + +// Init initializes the playerlist manager +func Init() (*PlayerlistManager, error) { + plm := &PlayerlistManager{ + Players: make(map[string]Player), + } + + plm.mu.Lock() + defer plm.mu.Unlock() + + // Create a file lock + lock := flock.New("playerlist.json.lock") + if err := lock.Lock(); err != nil { + log.Logger.Error("Error locking playerlist file", "error", err) + return nil, err + } + defer lock.Unlock() + + // Open or create the playerlist.json file + file, err := os.OpenFile("playerlist.json", os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + log.Logger.Error("Error opening/creating playerlist", "error", err) + return nil, err + } + defer file.Close() + + // Check if the file is empty (newly created) + info, err := file.Stat() + if err != nil { + log.Logger.Error("Error stating playerlist file", "error", err) + return nil, err + } + if info.Size() == 0 { + data, err := json.Marshal(plm.Players) + if err != nil { + log.Logger.Error("Error encoding default playerlist", "error", err) + return nil, err + } + if _, err := file.Write(data); err != nil { + log.Logger.Error("Error writing encoded default playerlist", "error", err) + return nil, err + } + } else { + // Read the existing data from the file + data := make([]byte, info.Size()) + if _, err := file.Read(data); err != nil { + log.Logger.Error("Error reading playerlist", "error", err) + return nil, err + } + + // Unmarshal the data into the player map + if err := json.Unmarshal(data, &plm.Players); err != nil { + log.Logger.Error("Error decoding playerlist", "error", err) + return nil, err + } + } + + return plm, nil +} + +// GetConnIdentityData returns the identity data for a player's connection +func (plm *PlayerlistManager) GetConnIdentityData(conn *minecraft.Conn) (login.IdentityData, error) { + plm.mu.Lock() + defer plm.mu.Unlock() // Keep mutex locked for the entire operation + + identityData := conn.IdentityData() + xuid := identityData.XUID + + if player, ok := plm.Players[xuid]; ok { + return login.IdentityData{ + XUID: xuid, + DisplayName: player.PlayerName, + Identity: player.Identity, + TitleID: identityData.TitleID, + }, nil + } + + // Set player with mutex still locked + player := Player{ + PlayerName: conn.IdentityData().DisplayName, + Identity: conn.IdentityData().Identity, + ClientSelfSignedID: conn.ClientData().SelfSignedID, + } + plm.Players[xuid] = player + + // Save the playerlist to disk + err := plm.savePlayerlist() + if err != nil { + return login.IdentityData{}, err + } + + return identityData, nil +} + +// GetConnClientData returns the client data for a player's connection +func (plm *PlayerlistManager) GetConnClientData(conn *minecraft.Conn) (login.ClientData, error) { + plm.mu.Lock() + defer plm.mu.Unlock() // Keep mutex locked for the entire operation + + xuid := conn.IdentityData().XUID + clientData := conn.ClientData() + + if player, ok := plm.Players[xuid]; ok { + clientData.SelfSignedID = player.ClientSelfSignedID + return clientData, nil + } + + // Set player with mutex still locked + player := Player{ + PlayerName: conn.IdentityData().DisplayName, + Identity: conn.IdentityData().Identity, + ClientSelfSignedID: conn.ClientData().SelfSignedID, + } + plm.Players[xuid] = player + + // Save the playerlist to disk + err := plm.savePlayerlist() + if err != nil { + return login.ClientData{}, err + } + + return clientData, nil +} + +// GetXUIDFromName returns the XUID of a player by their name +func (plm *PlayerlistManager) GetXUIDFromName(playerName string) (string, error) { + plm.mu.Lock() + defer plm.mu.Unlock() + + for xuid, player := range plm.Players { + if player.PlayerName == playerName { + return xuid, nil + } + } + + return "", errors.New("player not found") +} + +// GetPlayer returns a player from the playerlist by their XUID +func (plm *PlayerlistManager) GetPlayer(xuid string) (Player, error) { + plm.mu.Lock() + defer plm.mu.Unlock() + + player, ok := plm.Players[xuid] + if !ok { + return Player{}, errors.New("player not found") + } + + return player, nil +} + +// savePlayerlist saves the current playerlist to the JSON file +func (plm *PlayerlistManager) savePlayerlist() error { + // Create a file lock + lock := flock.New("playerlist.json.lock") + if err := lock.Lock(); err != nil { + log.Logger.Error("Error locking playerlist file", "error", err) + return err + } + + defer func() { + if err := lock.Unlock(); err != nil { + log.Logger.Error("Error unlocking playerlist file", "error", err) + } + }() + + // Save the playerlist to disk + data, err := json.MarshalIndent(plm.Players, "", " ") + if err != nil { + log.Logger.Error("Error encoding playerlist", "error", err) + return err + } + + // Open the file for writing + file, err := os.OpenFile("playerlist.json", os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + log.Logger.Error("Error opening playerlist for writing", "error", err) + return err + } + defer file.Close() + + if _, err := file.Write(data); err != nil { + log.Logger.Error("Error writing playerlist", "error", err) + return err + } + + return nil +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 6a0150e..dd5d24d 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -1,25 +1,33 @@ package proxy import ( + "bytes" + "context" + "encoding/json" "errors" - "runtime" + "fmt" + "net/http" + "runtime/debug" "strings" "sync" + "time" "github.com/HyPE-Network/vanilla-proxy/handler" "github.com/HyPE-Network/vanilla-proxy/log" "github.com/HyPE-Network/vanilla-proxy/math" - "github.com/HyPE-Network/vanilla-proxy/proxy/block" - "github.com/HyPE-Network/vanilla-proxy/proxy/command" - "github.com/HyPE-Network/vanilla-proxy/proxy/console" - "github.com/HyPE-Network/vanilla-proxy/proxy/console/bash" - "github.com/HyPE-Network/vanilla-proxy/proxy/console/bots" + "github.com/HyPE-Network/vanilla-proxy/proxy/entity" + "github.com/HyPE-Network/vanilla-proxy/proxy/player" "github.com/HyPE-Network/vanilla-proxy/proxy/player/human" + "github.com/HyPE-Network/vanilla-proxy/proxy/playerlist" "github.com/HyPE-Network/vanilla-proxy/proxy/whitelist" "github.com/HyPE-Network/vanilla-proxy/proxy/world" "github.com/HyPE-Network/vanilla-proxy/utils" + "github.com/google/uuid" + "github.com/sandertv/go-raknet" "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + "github.com/sandertv/gophertunnel/minecraft/resource" "github.com/sandertv/gophertunnel/minecraft" ) @@ -27,143 +35,286 @@ import ( var ProxyInstance *Proxy type Proxy struct { - Worlds *world.Worlds - Config utils.Config - PlayerManager human.HumanManager - Handlers handler.HandlerManager - CommandManager *command.CommandManager - Listener *minecraft.Listener - CommandSender console.CommandSender - WhitelistManager *whitelist.WhitelistManager + Worlds *world.Worlds + Entities *entity.Entities + Config utils.Config + Handlers handler.HandlerManager + Listener *minecraft.Listener + WhitelistManager *whitelist.WhitelistManager + PlayerListManager *playerlist.PlayerlistManager + ResourcePacks []*resource.Pack + ctx context.Context + cancel context.CancelFunc + cleanupTasks []func() + tasksMu sync.Mutex } -func New(config utils.Config, hm human.HumanManager) *Proxy { - block.Init() +func New(config utils.Config) *Proxy { + playerListManager, err := playerlist.Init() + if err != nil { + log.Logger.Error("Error in initializing playerListManager", "error", err) + } - commandManager := command.InitManager(config.Server.Ops) + ctx, cancel := context.WithCancel(context.Background()) Proxy := &Proxy{ - Config: config, - PlayerManager: hm, - CommandManager: commandManager, + Config: config, + Entities: entity.Init(), + PlayerListManager: playerListManager, + WhitelistManager: whitelist.Init(), + ctx: ctx, + cancel: cancel, + cleanupTasks: make([]func(), 0), } - if config.WorldBorder.Enabled { - Proxy.Worlds = world.Init(math.NewArea2(config.WorldBorder.MinX, config.WorldBorder.MinZ, config.WorldBorder.MaxX, config.WorldBorder.MaxZ)) - } + // Initialize an empty slice of *resource.Pack + var resourcePacks []*resource.Pack - if config.Server.Whitelist { - Proxy.WhitelistManager = whitelist.Init(commandManager) + // Loop through all the pack URLs and append each pack to the slice + for _, url := range Proxy.Config.Resources.PackURLs { + log.Logger.Debug("Reading resource pack from URL", "url", url) + resourcePack, err := resource.ReadURL(url) + if err != nil { + log.Logger.Error("Failed to read resource pack from URL", "url", url, "error", err) + } + resourcePacks = append(resourcePacks, resourcePack) } - os := runtime.GOOS - switch os { - case "windows": - if Proxy.Config.Bot.Enabled { - log.Logger.Debugln("Creating new console bot instance..") - Proxy.CommandSender = bots.NewBot(Proxy.Config) - } else { - log.Logger.Warnln("Console bot is disabled in config") + // Loop through all the pack paths and append each pack to the slice + for _, path := range Proxy.Config.Resources.PackPaths { + log.Logger.Debug("Reading resource pack from path", "path", path) + resourcePack, err := resource.ReadPath(path) + if err != nil { + log.Logger.Error("Failed to read resource pack from path", "path", path, "error", err) } - case "linux": - log.Logger.Debugln("Creating new bash console instance..") - Proxy.CommandSender = bash.NewBash(strings.Split(Proxy.Config.Connection.RemoteAddress, ":")[1]) + resourcePacks = append(resourcePacks, resourcePack) } - if Proxy.CommandSender == nil { - log.Logger.Warnln("CommandSender is not declared, functionality will be disabled") + Proxy.ResourcePacks = resourcePacks + + if config.WorldBorder.Enabled { + Proxy.Worlds = world.Init(math.NewArea2(config.WorldBorder.MinX, config.WorldBorder.MinZ, config.WorldBorder.MaxX, config.WorldBorder.MaxZ)) } return Proxy } +// The following program implements a proxy that forwards players from one local address to a remote address. func (arg *Proxy) Start(h handler.HandlerManager) error { - arg.Handlers = h - - if arg.Config.Rcon.Enabled { - go command.InitRCON(arg.CommandManager.Commands, arg.Config.Rcon.Port, arg.Config.Rcon.Password) + res, err := raknet.Ping(arg.Config.Connection.RemoteAddress) + if err != nil { + // Server prob not online, retrying + log.Logger.Warn("Failed to ping server, retrying in 5 seconds", "error", err) + time.Sleep(time.Second * 5) + arg.Start(h) + return nil } - + // Server is online, parse data + status := minecraft.ParsePongData(res) + log.Logger.Info("Server online", "name", status.ServerName, "motd", status.ServerSubName) p, err := minecraft.NewForeignStatusProvider(arg.Config.Connection.RemoteAddress) if err != nil { - return err + return fmt.Errorf("failed to create foreign status provider: %w", err) } + arg.Listener, err = minecraft.ListenConfig{ // server settings AuthenticationDisabled: arg.Config.Server.DisableXboxAuth, StatusProvider: p, + ResourcePacks: arg.ResourcePacks, + TexturePacksRequired: true, + ErrorLog: log.Logger, + Compression: packet.FlateCompression, + FlushRate: time.Millisecond * time.Duration(arg.Config.Server.FlushRate), }.Listen("raknet", arg.Config.Connection.ProxyAddress) if err != nil { - return err + return fmt.Errorf("failed to start listener: %w", err) } - defer arg.Stop() - - log.Logger.Debugln("Original server address:", arg.Config.Connection.RemoteAddress, "public address:", arg.Config.Connection.ProxyAddress) - log.Logger.Println("Proxy has been started on Version", protocol.CurrentVersion, "protocol", protocol.CurrentProtocol) + log.Logger.Debug("Original server address", "remote", arg.Config.Connection.RemoteAddress, "public", arg.Config.Connection.ProxyAddress) + log.Logger.Info("Proxy started", "version", protocol.CurrentVersion, "protocol", protocol.CurrentProtocol) + arg.Handlers = h + defer func() { + if r := recover(); r != nil { + log.Logger.Error("Recovered from panic in Handling Listener", "error", r, "stack", debug.Stack()) + log.Logger.Error("Closing listener", "error", arg.Listener.Close()) + } + }() for { - c, err := arg.Listener.Accept() + select { + case <-arg.ctx.Done(): + log.Logger.Info("Proxy shutting down") + return nil + default: + c, err := arg.Listener.Accept() + if err != nil { + if arg.ctx.Err() != nil { + return nil // Exit if context is cancelled + } - if err != nil { - log.Logger.Errorln(err) - continue - } + // The listener closed, so we should restart it. c==nil + log.Logger.Error("Listener accept error", "error", err) + utils.SendStaffAlertToDiscord("Proxy Listener Closed", "```"+err.Error()+"```", 16711680, []map[string]interface{}{}) - go arg.handleConn(c.(*minecraft.Conn)) - } -} + return arg.Start(h) + } + + if c == nil { + log.Logger.Warn("Accepted a nil connection") + continue + } -func (arg *Proxy) Stop() { - arg.CommandSender.Close() - arg.PlayerManager.DeleteAll() - arg.Listener.Close() + log.Logger.Debug("New connection", "addr", c.RemoteAddr()) + go arg.handleConn(c.(*minecraft.Conn)) + } + } } +// handleConn handles a new incoming minecraft.Conn from the minecraft.Listener passed. func (arg *Proxy) handleConn(conn *minecraft.Conn) { - if human, ok := arg.PlayerManager.PlayerList()[conn.IdentityData().DisplayName]; ok { // if the user is already in the system - err := conn.Close() - if err != nil { - log.Logger.Errorln(err) + defer func() { + if r := recover(); r != nil { + log.Logger.Error("Recovered from panic in handleConn", "error", r, "stack", debug.Stack()) + if conn != nil { + arg.Listener.Disconnect(conn, "An internal error occurred") + } } + }() - err = arg.Listener.Disconnect(conn, "connection lost") - if err != nil { - log.Logger.Errorln(err) + if conn == nil { + log.Logger.Warn("Received nil connection. Skipping handling") + return + } + + go func() { + <-arg.ctx.Done() + if conn != nil { + conn.Close() // Ensure the connection is closed when context is cancelled + } + }() + + playerWhitelisted := arg.WhitelistManager.HasPlayer(conn.IdentityData().DisplayName, conn.IdentityData().XUID) + if arg.Config.Server.Whitelist { + if !playerWhitelisted { + arg.Listener.Disconnect(conn, "You are not whitelisted on this server!") + return } + } - arg.deletePlayer(human) + res, err := raknet.Ping(arg.Config.Connection.RemoteAddress) + if err != nil { + // Server just went offline while player was connecting + arg.Listener.Disconnect(conn, "Server just went offline, please try again later!") + return + } + // Server is online, fetch data + status := minecraft.ParsePongData(res) + if !arg.canJoinServer(status, conn, playerWhitelisted) { + return + } + + clientData, err := arg.PlayerListManager.GetConnClientData(conn) + if err != nil { + log.Logger.Error("Error in getting clientData", "error", err) + arg.Listener.Disconnect(conn, strings.Split(err.Error(), ": ")[1]) + return + } + identityData, err := arg.PlayerListManager.GetConnIdentityData(conn) + if err != nil { + log.Logger.Error("Error in getting identityData", "error", err) + arg.Listener.Disconnect(conn, strings.Split(err.Error(), ": ")[1]) return } serverConn, err := minecraft.Dialer{ KeepXBLIdentityData: true, - ClientData: conn.ClientData(), - IdentityData: conn.IdentityData(), - }.Dial("raknet", arg.Config.Connection.RemoteAddress) + ClientData: clientData, + IdentityData: identityData, + DownloadResourcePack: func(id uuid.UUID, version string, current int, total int) bool { + return false + }, + ErrorLog: log.Logger, + }.DialTimeout("raknet", arg.Config.Connection.RemoteAddress, time.Second*30) if err != nil { - log.Logger.Errorln(err) + log.Logger.Error("Failed to dial server", "error", err) return } - gd := serverConn.GameData() - gd.WorldSeed = 0 - gd.ClientSideGeneration = false + log.Logger.Debug("Server connection established", "player", serverConn.IdentityData().DisplayName) + + if !arg.initializeConnection(conn, serverConn) { + return + } + + playerXuid := conn.IdentityData().XUID + if playerXuid == "" { + log.Logger.Error("Player XUID is empty, disconnecting player") + arg.Listener.Disconnect(conn, "Failed to get your XUID, please try again!") + return + } + + player := player.GetPlayer(conn, serverConn) + log.Logger.Info("Player joined", "name", player.GetName()) + player.SendXUIDToAddon() + arg.UpdatePlayerDetails(player) + + arg.startPacketHandlers(player, conn, serverConn) +} + +// canJoinServer checks if a player can join the server based on its status. +// It returns true if the player can join, and false if the player can't join. +// If false, the player will be disconnected with a message. +func (arg *Proxy) canJoinServer(status minecraft.ServerStatus, conn *minecraft.Conn, whitelisted bool) bool { + if status.PlayerCount >= status.MaxPlayers-arg.Config.Server.SecuredSlots { + if whitelisted && status.PlayerCount >= status.MaxPlayers { + // Player is whitelisted, but all secured slots are taken too, so we can't let them in + arg.Listener.Disconnect(conn, fmt.Sprintf("Sorry %s, even though you have priority access, all secured slots are taken! (%d/%d)", conn.IdentityData().DisplayName, status.PlayerCount, status.MaxPlayers)) + return false + } else if !whitelisted && status.PlayerCount < status.MaxPlayers { + // Player is not whitelisted, but the server is full to non whitelisted players. + arg.Listener.Disconnect(conn, fmt.Sprintf("Sorry %s, even though the server is not full, the remaining slots are reserved for our staff! (%d/%d)", conn.IdentityData().DisplayName, status.PlayerCount, status.MaxPlayers)) + return false + } else if !whitelisted { + // Player is not whitelisted and the server is completely full. + arg.Listener.Disconnect(conn, fmt.Sprintf("Sorry %s, the server is full, please try again later! (%d/%d)", conn.IdentityData().DisplayName, status.PlayerCount, status.MaxPlayers)) + return false + } + // Player is whitelisted and there are secured slots available, let them in + } + return true +} + +// initializeConnection handles the initial setup for a new connection. +// It returns true if the connection was successfully established, and false if it wasn't. +// If false, the player will be disconnected with a message. +func (arg *Proxy) initializeConnection(conn *minecraft.Conn, serverConn *minecraft.Conn) bool { + gameData := serverConn.GameData() + gameData.WorldSeed = 0 + gameData.ClientSideGeneration = false + arg.Worlds.SetItems(gameData.Items) + arg.Worlds.SetCustomBlocks(gameData.CustomBlocks) var success = true var g sync.WaitGroup g.Add(2) go func() { - if err := conn.StartGame(gd); err != nil { - log.Logger.Errorln(err) + if err := conn.StartGame(gameData); err != nil { + var disc minecraft.DisconnectError + if ok := errors.As(err, &disc); !ok { + log.Logger.Error("Failed to start game on client", "error", err) + } success = false } g.Done() }() go func() { if err := serverConn.DoSpawn(); err != nil { - log.Logger.Errorln(err) + var disc minecraft.DisconnectError + if ok := errors.As(err, &disc); !ok { + log.Logger.Error("Failed to spawn client on server", "error", err) + } success = false } g.Done() @@ -171,94 +322,263 @@ func (arg *Proxy) handleConn(conn *minecraft.Conn) { g.Wait() if !success { - arg.CloseConnections(conn, serverConn) - return + arg.Listener.Disconnect(conn, "Failed to establish a connection, please try again!") + serverConn.Close() + return false } - if arg.Config.Server.Whitelist { - if !arg.WhitelistManager.HasPlayer(conn.IdentityData().DisplayName, conn.IdentityData().XUID) { - arg.CloseConnections(conn, serverConn) - return - } - } + return true +} + +func (arg *Proxy) startPacketHandlers(player human.Human, conn *minecraft.Conn, serverConn *minecraft.Conn) { + go func() { // client->proxy + defer func() { + reason := "Client Connection closed" + if r := recover(); r != nil { + log.Logger.Error("Recovered from panic in HandlePacket from Client", "error", r, "stack", debug.Stack()) + reason = "An internal error occurred" + } - pl := arg.PlayerManager.AddPlayer(conn, serverConn) - log.Logger.Infoln(pl.GetName(), "joined the server") + // Order is Important. We want to close the client connection first + arg.PrePlayerDisconnect(player) + arg.Listener.Disconnect(conn, reason) + serverConn.Close() + }() - go func() { // client-proxy for { - pk, err := conn.ReadPacket() - if err != nil { - break - } + select { + case <-arg.ctx.Done(): + return + default: + pk, err := conn.ReadPacket() + if err != nil { + arg.handlePacketError(err, conn, "Failed to read packet from client") + return + } - ok, pk, err := arg.Handlers.HandlePacket(pk, pl, "Client") - if err != nil { - log.Logger.Errorln(err) - } + ok, pk, err := arg.Handlers.HandlePacket(pk, player, "Client") + if err != nil { + log.Logger.Error("Error handling packet from client", "error", err) + } - if ok { - if err := serverConn.WritePacket(pk); err != nil { - if disconnect, ok := errors.Unwrap(err).(minecraft.DisconnectError); ok { - _ = arg.Listener.Disconnect(conn, disconnect.Error()) + if ok { + if err := serverConn.WritePacket(pk); err != nil { + arg.handlePacketError(err, conn, "Failed to write packet to proxy") + return } - break } } } - arg.deletePlayer(pl) }() - go func() { // proxy-server + + go func() { // proxy->server + defer func() { + reason := "Server Connection closed" + if r := recover(); r != nil { + log.Logger.Error("Recovered from panic in HandlePacket from Server", "error", r, "stack", debug.Stack()) + reason = "An internal error occurred" + } + + // Order is Important. We want to close the server connection first + arg.PrePlayerDisconnect(player) + serverConn.Close() + arg.Listener.Disconnect(conn, reason) + }() + for { - pk, err := serverConn.ReadPacket() - if err != nil { - if disconnect, ok := errors.Unwrap(err).(minecraft.DisconnectError); ok { - _ = arg.Listener.Disconnect(conn, disconnect.Error()) + select { + case <-arg.ctx.Done(): + return + default: + pk, err := serverConn.ReadPacket() + if err != nil { + arg.handlePacketError(err, conn, "Failed to read packet from proxy") + return } - break - } - ok, pk, err := arg.Handlers.HandlePacket(pk, pl, "Server") - if err != nil { - log.Logger.Errorln(err) - } + ok, pk, err := arg.Handlers.HandlePacket(pk, player, "Server") + if err != nil { + log.Logger.Error("Error", "error", err) + } - if ok { - if err := conn.WritePacket(pk); err != nil { - break + if ok { + if err := conn.WritePacket(pk); err != nil { + arg.handlePacketError(err, conn, "Failed to write packet to server") + return + } } } } - arg.deletePlayer(pl) }() } -func (arg *Proxy) deletePlayer(human human.Human) { - arg.PlayerManager.DeletePlayer(human) - arg.Listener.Disconnect(human.GetSession().Connection.ClientConn, "connection lost") +// RegisterCleanupTask adds a function to be called during proxy shutdown +func (arg *Proxy) RegisterCleanupTask(task func()) { + arg.tasksMu.Lock() + defer arg.tasksMu.Unlock() + arg.cleanupTasks = append(arg.cleanupTasks, task) +} + +func (arg *Proxy) Shutdown() { + log.Logger.Info("Shutting down proxy") + + // Run all cleanup tasks first + arg.tasksMu.Lock() + for _, task := range arg.cleanupTasks { + task() + } + arg.tasksMu.Unlock() + + arg.cancel() // This will cancel the context and stop all goroutines + if arg.Listener != nil { + arg.Listener.Close() // Close the listener if it's open + } +} + +// handlePacketError handles an error that occurred while reading a packet. +func (arg *Proxy) handlePacketError(err error, conn *minecraft.Conn, msg string) { + var disc minecraft.DisconnectError + if ok := errors.As(err, &disc); ok { + arg.Listener.Disconnect(conn, disc.Error()) + } + if !strings.Contains(err.Error(), "use of closed network connection") && !strings.Contains(err.Error(), "disconnect.kicked.reason") { + // Error is not a disconnect error, so log the error. + log.Logger.Error("Error", "msg", msg, "error", err) + } } -func (arg *Proxy) SendConsoleCommand(cmd string) { - if arg.CommandSender == nil { - log.Logger.Warnln("CommandSender is not declared. Ignored:", cmd) - } else { - if err := arg.CommandSender.SendCommand(cmd); err != nil { - log.Logger.Errorln("CommandSender:", err) +// PrePlayerDisconnect handles last second checks before a player disconnects. +func (arg *Proxy) PrePlayerDisconnect(player human.Human) { + // Send close container packet + if player.IsBeingDisconnected() { + return // Player is already being disconnected, ignore this call + } + player.SetDisconnected(true) + + openContainerId := player.GetData().OpenContainerWindowId + itemInContainers := player.GetData().ItemsInContainers + playerLastLocation := player.GetData().LastUpdatedLocation + lastLocationString := fmt.Sprintf("[%d, %d, %d]", int(playerLastLocation.X()), int(playerLastLocation.Y()), int(playerLastLocation.Z())) + + if openContainerId != 0 && len(itemInContainers) > 0 { + log.Logger.Info("Player disconnecting with open container", "name", player.GetName(), "container", openContainerId, "location", lastLocationString) + + // utils.SendStaffAlertToDiscord("Disconnect with open container!", "A Player Has disconnected with an open container, please investigate!", 16711680, []map[string]interface{}{ + // { + // "name": "Player Name", + // "value": "```" + player.GetName() + "```", + // "inline": true, + // }, + // { + // "name": "Player Location", + // "value": "```" + lastLocationString + "```", + // "inline": true, + // }, + // { + // "name": "Item Count", + // "value": "```" + fmt.Sprintf("%d", len(itemInContainers)) + "```", + // "inline": true, + // }, + // }) + + // Send Item Stack Requests to clear the container + // Send Item Request to clear container id 13 (crafting table) + // By sending from slot 32->40 (9 crafting slots) to `false` (throw on ground) + request := protocol.ItemStackRequest{ + RequestID: player.GetNextItemStackRequestID(), + Actions: []protocol.StackRequestAction{}, } + // Loop through players container slots + for _, slotInfo := range itemInContainers { + action := &protocol.DropStackRequestAction{} + action.Source = slotInfo + action.Count = 64 + action.Randomly = false + request.Actions = append(request.Actions, action) + } + pk := &packet.ItemStackRequest{ + Requests: []protocol.ItemStackRequest{request}, + } + log.Logger.Debug("Sending ItemStackRequest to clear container") + player.DataPacketToServer(pk) + + player.SetOpenContainerWindowID(0) + player.SetOpenContainerType(0) + + // Sleep for 2 seconds to allow the packets to be sent + time.Sleep(time.Second * 4) } + + cursorItem := player.GetItemFromContainerSlot(protocol.ContainerCombinedHotBarAndInventory, 0) + if cursorItem.StackNetworkID != 0 { + // Player left with a item in ContainerCombinedHotBarAndInventory + utils.SendStaffAlertToDiscord("Disconnecting With Item", "A Player Has disconnected with a item in ContainerCombinedHotBarAndInventory, please investigate!", 16711680, []map[string]interface{}{ + { + "name": "Player Name", + "value": "```" + player.GetName() + "```", + "inline": true, + }, + { + "name": "Stack Network ID", + "value": "```" + fmt.Sprintf("%d", cursorItem.StackNetworkID) + "```", + "inline": true, + }, + { + "name": "Player Location", + "value": "```" + lastLocationString + "```", + "inline": true, + }, + }) + } +} + +type PlayerDetails struct { + Xuid string `json:"xuid"` + Name string `json:"name"` + IP string `json:"ip"` } -func (arg *Proxy) CloseConnections(conn *minecraft.Conn, serverConn *minecraft.Conn) { - err := conn.Close() +func (arg *Proxy) UpdatePlayerDetails(player human.Human) { + xuid := player.GetSession().IdentityData.XUID + + // Build the URI for the API request + uri := arg.Config.Api.ApiHost + "/api/moderation/playerDetails" + log.Logger.Info("Sending playerDetails", "name", player.GetName(), "uri", uri) + + // Create the player details payload + playerDetails := PlayerDetails{ + Xuid: xuid, + Name: player.GetName(), + IP: strings.Split(player.GetSession().Connection.ClientConn.RemoteAddr().String(), ":")[0], + } + + // Convert player details to JSON + jsonData, err := json.Marshal(playerDetails) if err != nil { - log.Logger.Errorln(err) + log.Logger.Error("Failed to marshal player details", "error", err) + return } - err = serverConn.Close() + + // Create a new HTTP POST request + req, err := http.NewRequest("POST", uri, bytes.NewBuffer(jsonData)) if err != nil { - log.Logger.Errorln(err) + log.Logger.Error("Failed to create new request", "error", err) + return } - err = arg.Listener.Disconnect(conn, "connection lost") + + // Set the headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("authorization", arg.Config.Api.ApiKey) + + // Send the request + client := &http.Client{} + resp, err := client.Do(req) if err != nil { - log.Logger.Errorln(err) + log.Logger.Error("Failed to send request", "error", err) + return } + defer resp.Body.Close() + + // Log the response status + log.Logger.Info("Sent playerDetails", "uri", uri, "status", resp.StatusCode) } diff --git a/proxy/scheduler/broadcaster/broadcast_task.go b/proxy/scheduler/broadcaster/broadcast_task.go deleted file mode 100644 index 85be169..0000000 --- a/proxy/scheduler/broadcaster/broadcast_task.go +++ /dev/null @@ -1,24 +0,0 @@ -package broadcaster - -import ( - "github.com/HyPE-Network/vanilla-proxy/proxy/scheduler" - "github.com/HyPE-Network/vanilla-proxy/server" -) - -var messages []string -var timer = 0 - -func Init() { - scheduler.NewRepeatingTask(60*5, Broadcast) - messages = []string{} -} - -func Broadcast() { - if timer >= len(messages) { - timer = 0 - } - - text := messages[timer] - timer += 1 - server.BroadcastMessage(text) -} diff --git a/proxy/session/session.go b/proxy/session/session.go index 1a2f76f..800e17d 100644 --- a/proxy/session/session.go +++ b/proxy/session/session.go @@ -11,7 +11,9 @@ type Session struct { Connection *Bridge } -func NewSession(identityData login.IdentityData, clientData login.ClientData, connection *Bridge) *Session { +func NewSession(conn *minecraft.Conn, connection *Bridge) *Session { + identityData := conn.IdentityData() + clientData := conn.ClientData() return &Session{IdentityData: identityData, ClientData: clientData, Connection: connection} } diff --git a/proxy/sound/sound.go b/proxy/sound/sound.go deleted file mode 100644 index 54393d8..0000000 --- a/proxy/sound/sound.go +++ /dev/null @@ -1,19 +0,0 @@ -package sound - -import ( - "github.com/go-gl/mathgl/mgl32" - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -func GetSound(soundId uint32, pos mgl32.Vec3) *packet.LevelSoundEvent { - pk := &packet.LevelSoundEvent{ - SoundType: soundId, - Position: pos, - ExtraData: -1, - EntityType: ":", - BabyMob: false, - DisableRelativeVolume: false, - } - - return pk -} diff --git a/proxy/whitelist/whitelist.go b/proxy/whitelist/whitelist.go index e652066..90665c4 100644 --- a/proxy/whitelist/whitelist.go +++ b/proxy/whitelist/whitelist.go @@ -2,64 +2,62 @@ package whitelist import ( "encoding/json" - "log" "os" "strings" + "sync" - "github.com/HyPE-Network/vanilla-proxy/proxy/command" - - "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/HyPE-Network/vanilla-proxy/log" ) type WhitelistManager struct { + mu sync.Mutex Players map[string]string } -func Init(commandManager *command.CommandManager) *WhitelistManager { +func Init() *WhitelistManager { wm := &WhitelistManager{ Players: make(map[string]string, 0), } + wm.mu.Lock() + defer wm.mu.Unlock() + if _, err := os.Stat("whitelist.json"); os.IsNotExist(err) { f, err := os.Create("whitelist.json") if err != nil { - log.Fatalf("error creating whitelist: %v", err) + log.Logger.Error("Error creating whitelist", "error", err) + panic(err) } - data, err := json.Marshal(wm) + data, err := json.Marshal(wm.Players) if err != nil { - log.Fatalf("error encoding default whitelist: %v", err) + log.Logger.Error("Error encoding default whitelist", "error", err) + panic(err) } if _, err := f.Write(data); err != nil { - log.Fatalf("error writing encoded default whitelist: %v", err) + log.Logger.Error("Error writing encoded default whitelist", "error", err) + panic(err) } _ = f.Close() } data, err := os.ReadFile("whitelist.json") if err != nil { - log.Fatalf("error reading whitelist: %v", err) + log.Logger.Error("Error reading whitelist", "error", err) + panic(err) } - if err := json.Unmarshal(data, wm); err != nil { - log.Fatalf("error decoding whitelist: %v", err) + if err := json.Unmarshal(data, &wm.Players); err != nil { + log.Logger.Error("Error decoding whitelist", "error", err) + panic(err) } - for _, name := range commandManager.Ops { - if _, ok := wm.Players[name]; !ok { - wm.AddPlayer(name) - } - } - - commandManager.RegisterCommand(protocol.Command{ - Name: "whitelist", - Description: "Server whitelist", - Overloads: []protocol.CommandOverload{}, - }, WhitelistCommandExecutor{WhitelistManager: wm}) - return wm } func (wm *WhitelistManager) HasPlayer(name string, xuid string) bool { + wm.mu.Lock() + defer wm.mu.Unlock() + for player_name, player_xuid := range wm.Players { if strings.EqualFold(player_xuid, xuid) { if !strings.EqualFold(player_name, name) { @@ -67,7 +65,6 @@ func (wm *WhitelistManager) HasPlayer(name string, xuid string) bool { wm.Players[name] = xuid wm.save() } - return true } @@ -76,17 +73,6 @@ func (wm *WhitelistManager) HasPlayer(name string, xuid string) bool { wm.Players[player_name] = xuid wm.save() } - - return true - } - } - - return false -} - -func (wm *WhitelistManager) HasPlayerName(name string) bool { - for player_name := range wm.Players { - if strings.EqualFold(player_name, name) { return true } } @@ -94,40 +80,24 @@ func (wm *WhitelistManager) HasPlayerName(name string) bool { return false } +// save saves the whitelist to the whitelist.json file, mutex must be held by the calling function. func (wm *WhitelistManager) save() { file, err := os.Create("whitelist.json") if err != nil { - log.Fatalf("error reading whitelist: %v", err) + log.Logger.Error("Error creating whitelist file", "error", err) + panic(err) } - p, err := json.MarshalIndent(wm, "", "\t") + p, err := json.MarshalIndent(wm.Players, "", "\t") if err != nil { - log.Fatalf("error marshal whitelist: %v", err) + log.Logger.Error("Error marshaling whitelist", "error", err) + panic(err) } _, err = file.Write(p) if err != nil { - log.Fatalf("error write whitelist: %v", err) + log.Logger.Error("Error writing whitelist", "error", err) + panic(err) } file.Close() } - -func (wm *WhitelistManager) AddPlayer(name string) bool { - if wm.HasPlayerName(name) { - return false - } - - wm.Players[strings.ToLower(name)] = "none" - wm.save() - return true -} - -func (wm *WhitelistManager) RemovePlayer(name string) bool { - if !wm.HasPlayerName(name) { - return false - } - - delete(wm.Players, strings.ToLower(name)) - wm.save() - return true -} diff --git a/proxy/whitelist/whitelist_command.go b/proxy/whitelist/whitelist_command.go deleted file mode 100644 index 3471753..0000000 --- a/proxy/whitelist/whitelist_command.go +++ /dev/null @@ -1,44 +0,0 @@ -package whitelist - -import ( - "fmt" - "strings" - - "github.com/HyPE-Network/vanilla-proxy/proxy/command/sender" -) - -type WhitelistCommandExecutor struct { - WhitelistManager *WhitelistManager -} - -func (wce WhitelistCommandExecutor) Execute(commandSender sender.CommandSender, args []string) error { - if len(args) < 2 { - return fmt.Errorf("not enough arguments") - } - - do := args[0] - player := strings.Join(args[1:], " ") - - switch do { - case "add": - ok := wce.WhitelistManager.AddPlayer(player) - if ok { - commandSender.SendMessage("Player added to whitelist!") - } else { - return fmt.Errorf("the player is already on the whitelist") - } - case "remove": - ok := wce.WhitelistManager.AddPlayer(player) - if ok { - commandSender.SendMessage("Player removed from whitelist!") - } else { - return fmt.Errorf("the player is not on the whitelist") - } - } - - return nil -} - -func (WhitelistCommandExecutor) ForPlayer() bool { - return false -} diff --git a/proxy/block/blockState.go b/proxy/world/block_state.go similarity index 98% rename from proxy/block/blockState.go rename to proxy/world/block_state.go index 92f54a9..47b086a 100644 --- a/proxy/block/blockState.go +++ b/proxy/world/block_state.go @@ -1,4 +1,4 @@ -package block +package world import ( "bytes" @@ -24,7 +24,7 @@ var ( AirRID uint32 ) -func Init() { +func InitBlocks() { dec := nbt.NewDecoder(bytes.NewBuffer(blockStateData)) BlockRuntimes = make(map[string]uint32) diff --git a/proxy/block/block_states.nbt b/proxy/world/block_states.nbt similarity index 100% rename from proxy/block/block_states.nbt rename to proxy/world/block_states.nbt diff --git a/proxy/world/world.go b/proxy/world/world.go index b3bbd24..08e7a91 100644 --- a/proxy/world/world.go +++ b/proxy/world/world.go @@ -1,15 +1,81 @@ package world import ( + "slices" + "github.com/HyPE-Network/vanilla-proxy/math" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) type Worlds struct { Border math.Area2 + // Commands that were sent by BDS and are available to the player. + BDSAvailableCommands packet.AvailableCommands + // ItemComponentEntries holds a list of all custom items with their respective components set. + ItemComponentEntries []protocol.ItemEntry + // Items holds a list of all custom items that are available in the server. + Items []protocol.ItemEntry + // CustomBlocks holds a list of all custom blocks that are available in the server. + CustomBlocks []protocol.BlockEntry } func Init(border *math.Area2) *Worlds { + InitBlocks() + return &Worlds{ Border: *border, } } + +func (worlds *Worlds) SetItems(items []protocol.ItemEntry) { + worlds.Items = items +} + +func (worlds *Worlds) GetItems() []protocol.ItemEntry { + return worlds.Items +} + +func (worlds *Worlds) SetCustomBlocks(blocks []protocol.BlockEntry) { + worlds.CustomBlocks = blocks +} + +func (worlds *Worlds) GetCustomBlocks() []protocol.BlockEntry { + return worlds.CustomBlocks +} + +// SetBDSAvailableCommands sets the AvailableCommands packet that is sent to the player when they join the server. +func (worlds *Worlds) SetBDSAvailableCommands(pk *packet.AvailableCommands) { + worlds.BDSAvailableCommands = *pk +} + +// GetItemEntry returns the item entry of an item with the specified network ID. If the item is not found, nil is returned. +func (worlds *Worlds) GetItemEntry(networkID int32) *protocol.ItemEntry { + items := worlds.Items + idx := slices.IndexFunc(items, func(item protocol.ItemEntry) bool { + return item.RuntimeID == int16(networkID) + }) + if idx == -1 { + // Unknown item? + return nil + } + item := items[idx] + return &item +} + +func (worlds *Worlds) GetItemComponentEntry(name string) *protocol.ItemEntry { + for _, entry := range worlds.ItemComponentEntries { + if entry.Name == name { + return &entry + } + } + return nil +} + +func (worlds *Worlds) GetItemComponentEntries() []protocol.ItemEntry { + return worlds.ItemComponentEntries +} + +func (worlds *Worlds) SetItemComponentEntries(entries []protocol.ItemEntry) { + worlds.ItemComponentEntries = entries +} diff --git a/server/commands/whitelist_command.go b/server/commands/whitelist_command.go deleted file mode 100644 index 1239131..0000000 --- a/server/commands/whitelist_command.go +++ /dev/null @@ -1,17 +0,0 @@ -package server - -import ( - "github.com/HyPE-Network/vanilla-proxy/proxy/command/sender" -) - -type WhitelistCommandExecutor struct { -} - -func (WhitelistCommandExecutor) Execute(commandSender sender.CommandSender, args []string) error { - - return nil -} - -func (WhitelistCommandExecutor) ForPlayer() bool { - return false -} diff --git a/server/server.go b/server/server.go deleted file mode 100644 index 9acd8f6..0000000 --- a/server/server.go +++ /dev/null @@ -1,46 +0,0 @@ -package server - -import ( - "github.com/HyPE-Network/vanilla-proxy/proxy" - - "github.com/sandertv/gophertunnel/minecraft/protocol/packet" -) - -func BroadcastMessage(message string) { - BroadcastPacket(CreateTextPacket(message, packet.TextTypeChat)) -} - -func BroadcastPopup(message string) { - BroadcastPacket(CreateTextPacket(message, packet.TextTypePopup)) -} - -func BroadcastTip(message string) { - BroadcastPacket(CreateTextPacket(message, packet.TextTypeTip)) -} - -func BroadcastTransfer(address string, port uint16) { - pk := &packet.Transfer{ - Address: address, - Port: port, - } - - BroadcastPacket(pk) -} - -func BroadcastPacket(pk packet.Packet) { - for _, pl := range proxy.ProxyInstance.PlayerManager.PlayerList() { - pl.DataPacket(pk) - } -} - -func CreateTextPacket(message string, textType byte) *packet.Text { - return &packet.Text{ - TextType: textType, - NeedsTranslation: false, - SourceName: "", - Message: message, - Parameters: []string{}, - XUID: "", - PlatformChatID: "", - } -} diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..941715f --- /dev/null +++ b/start.bat @@ -0,0 +1,10 @@ +@echo off + +:: Install the new packages +go get -u all + +:: Update packages used on your project +go mod tidy + +:: Run the project +go run main.go \ No newline at end of file diff --git a/utils/config.go b/utils/config.go index 9cc4ef3..f32a5e6 100644 --- a/utils/config.go +++ b/utils/config.go @@ -1,9 +1,12 @@ package utils import ( - "log" + "fmt" "os" + "strconv" + "strings" + "github.com/HyPE-Network/vanilla-proxy/log" "github.com/pelletier/go-toml" ) @@ -12,85 +15,216 @@ type Config struct { ProxyAddress string RemoteAddress string } - Bot struct { - Enabled bool - XUID string - DisplayName string - } - Rcon struct { - Enabled bool - Port int - Password string - } Server struct { + SecuredSlots int ViewDistance int32 Whitelist bool DisableXboxAuth bool - Ops []string + Prefix string + FlushRate int } WorldBorder struct { Enabled bool MinX int32 - MinZ int32 MaxX int32 + MinZ int32 MaxZ int32 } + Api struct { + ApiHost string + ApiKey string + XboxApiKey string + } + Resources struct { + PackURLs []string + PackPaths []string + } + Database struct { + Host string + Key string + Name string + } + Encryption struct { + Key string + } + Logging struct { + DiscordLoggingEnabled bool + DiscordCommandLogsWebhook string + DiscordChatLogsWebhook string + DiscordSignLogsWebhook string + DiscordStaffAlertsWebhook string + ProfilerHost string + SentryDsn string + } } func ReadConfig() Config { - c := Config{ + // Initialize with default values + defaultConfig := Config{ Connection: struct { ProxyAddress string RemoteAddress string - }{"0.0.0.0:19134", "0.0.0.0:19132"}, + }{ + ProxyAddress: "0.0.0.0:19132", + RemoteAddress: "0.0.0.0:19134", + }, + Server: struct { + SecuredSlots int + ViewDistance int32 + Whitelist bool + DisableXboxAuth bool + Prefix string + FlushRate int + }{ + SecuredSlots: 5, + ViewDistance: 10, + Whitelist: true, + DisableXboxAuth: false, + Prefix: "VANILLA", + FlushRate: 10, + }, + WorldBorder: struct { + Enabled bool + MinX int32 + MaxX int32 + MinZ int32 + MaxZ int32 + }{ + Enabled: true, + MinX: -12000, + MinZ: -12000, + MaxX: 12000, + MaxZ: 12000, + }, + Encryption: struct { + Key string + }{ + Key: "default_encryption_key_change_me", + }, + Logging: struct { + DiscordLoggingEnabled bool + DiscordCommandLogsWebhook string + DiscordChatLogsWebhook string + DiscordSignLogsWebhook string + DiscordStaffAlertsWebhook string + ProfilerHost string + SentryDsn string + }{ + DiscordLoggingEnabled: false, + ProfilerHost: "0.0.0.0:19135", + }, } if _, err := os.Stat("config.toml"); os.IsNotExist(err) { + log.Logger.Info("config.toml not found, creating default config") f, err := os.Create("config.toml") if err != nil { - log.Fatalf("error creating config: %v", err) + log.Logger.Error("Error creating config", "error", err) + panic(err) } - data, err := toml.Marshal(c) + data, err := toml.Marshal(defaultConfig) if err != nil { - log.Fatalf("error encoding default config: %v", err) + log.Logger.Error("Error encoding default config", "error", err) + panic(err) } if _, err := f.Write(data); err != nil { - log.Fatalf("error writing encoded default config: %v", err) + log.Logger.Error("Error writing encoded default config", "error", err) + panic(err) } _ = f.Close() } data, err := os.ReadFile("config.toml") if err != nil { - log.Fatalf("error reading config: %v", err) + log.Logger.Error("Error reading config", "error", err) + panic(err) } + c := Config{} if err := toml.Unmarshal(data, &c); err != nil { - log.Fatalf("error decoding config: %v", err) + log.Logger.Error("Error decoding config", "error", err) + panic(err) } + // Validate required fields and set defaults if necessary if c.Connection.ProxyAddress == "" { panic("ProxyAddress is not assigned in config!") } - if c.WorldBorder.Enabled && c.WorldBorder.MaxX == 0 && c.WorldBorder.MaxZ == 0 && c.WorldBorder.MinX == 0 && c.WorldBorder.MinZ == 0 { - c.WorldBorder.MaxX = 1200 - c.WorldBorder.MaxZ = 1200 - c.WorldBorder.MinX = -1200 - c.WorldBorder.MinZ = -1200 + if c.Connection.RemoteAddress == "" { + panic("RemoteAddress is not assigned in config!") + } + + if c.Server.SecuredSlots <= 0 { + panic("SecuredSlots must be a value greater than 0!") + } + + if c.Server.ViewDistance <= 0 { + panic("ViewDistance must be a value greater than 0!") + } + + if c.Server.FlushRate <= 0 { + panic("FlushRate must be a value greater than 0!") + } + + // Verify world border is set correctly + if c.WorldBorder.MinX >= c.WorldBorder.MaxX || c.WorldBorder.MinZ >= c.WorldBorder.MaxZ { + panic("WorldBorder is not set correctly!") + } + + if c.Api.ApiHost == "" { + panic("API Host must be a valid address!") } - if c.Server.ViewDistance == 0 { - c.Server.ViewDistance = 10 + if c.Api.ApiKey == "" { + panic("API Key must be provided!") } - if c.Rcon.Enabled && (c.Rcon.Port == 0 || c.Rcon.Password == "") { - panic("Rcon is enabled and not configured in config!") + if c.Api.XboxApiKey == "" { + panic("Xbox API Key must be provided for Xbox API authentication!") } - data, _ = toml.Marshal(c) - if err := os.WriteFile("config.toml", data, 0644); err != nil { - log.Fatalf("error writing config file: %v", err) + if c.Database.Host == "" { + panic("Database Host must be a valid address!") + } + + if c.Database.Key == "" { + panic("Database Key must be provided!") + } + + if c.Database.Name == "" { + panic("Database Name must be provided!") + } + + if c.Encryption.Key == "" || c.Encryption.Key == "default_encryption_key_change_me" { + log.Logger.Warn("Using default encryption key. Please change this in your config!") + } + + if c.Logging.DiscordLoggingEnabled { + if c.Logging.DiscordCommandLogsWebhook == "" { + panic("Discord Command Logs Webhook must be provided when Discord logging is enabled!") + } + + if c.Logging.DiscordChatLogsWebhook == "" { + panic("Discord Chat Logs Webhook must be provided when Discord logging is enabled!") + } + + if c.Logging.DiscordSignLogsWebhook == "" { + panic("Discord Sign Logs Webhook must be provided when Discord logging is enabled!") + } + + if c.Logging.DiscordStaffAlertsWebhook == "" { + panic("Discord Staff Alerts Webhook must be provided when Discord logging is enabled!") + } + } + + if c.Logging.ProfilerHost == "" { + // Set the port to 3 more than the proxy port + port, err := strconv.Atoi(strings.Split(c.Connection.ProxyAddress, ":")[1]) + if err != nil { + panic("Failed to parse proxy port: " + err.Error()) + } + c.Logging.ProfilerHost = fmt.Sprintf("0.0.0.0:%d", port+3) } return c diff --git a/utils/encryption.go b/utils/encryption.go new file mode 100644 index 0000000..9b5fce1 --- /dev/null +++ b/utils/encryption.go @@ -0,0 +1,31 @@ +package utils + +import ( + "crypto/md5" + "encoding/base64" +) + +// EncryptMessage encrypts a message using a simple XOR cipher with a key derived from MD5 +// and returns a base64 encoded string that only contains characters safe for Minecraft chat +func EncryptMessage(message, key string) (string, error) { + // Create a fixed-size encryption key by hashing the provided key + hasher := md5.New() + hasher.Write([]byte(key)) + keyBytes := hasher.Sum(nil) + + // Convert message to bytes + messageBytes := []byte(message) + + // Encrypt the message with a simple XOR cipher + encryptedBytes := make([]byte, len(messageBytes)) + for i := range messageBytes { + keyIndex := i % len(keyBytes) + encryptedBytes[i] = messageBytes[i] ^ keyBytes[keyIndex] + } + + // Encode with base64 using URL encoding to ensure compatibility with Minecraft + // URL encoding uses only alphanumeric characters plus some symbols + encoded := base64.URLEncoding.EncodeToString(encryptedBytes) + + return encoded, nil +} diff --git a/proxy/scheduler/scheduler.go b/utils/scheduler.go similarity index 55% rename from proxy/scheduler/scheduler.go rename to utils/scheduler.go index 99f905a..6fab868 100644 --- a/proxy/scheduler/scheduler.go +++ b/utils/scheduler.go @@ -1,4 +1,4 @@ -package scheduler +package utils import "time" @@ -11,14 +11,16 @@ type DelayedRepeatingTask struct { Delay int64 Seconds int64 action func() + stop chan struct{} } type RepeatingTask struct { Seconds int64 action func() + stop chan struct{} } -func NewDelayedTask(delay int64, action func()) { +func NewDelayedTask(delay int64, action func()) *DelayedTask { dt := &DelayedTask{ Delay: delay, action: action, @@ -28,17 +30,20 @@ func NewDelayedTask(delay int64, action func()) { time.Sleep(time.Duration(dt.Delay) * time.Second) dt.onRun() }() + + return dt } func (dt *DelayedTask) onRun() { dt.action() } -func NewDelayedRepeatingTask(delay, seconds int64, action func()) { +func NewDelayedRepeatingTask(delay, seconds int64, action func()) *DelayedRepeatingTask { drt := &DelayedRepeatingTask{ Delay: delay, Seconds: seconds, action: action, + stop: make(chan struct{}), } go func() { @@ -46,30 +51,53 @@ func NewDelayedRepeatingTask(delay, seconds int64, action func()) { drt.onRun() for { - time.Sleep(time.Duration(drt.Seconds) * time.Second) - drt.onRun() + select { + case <-drt.stop: + return + default: + time.Sleep(time.Duration(drt.Seconds) * time.Second) + drt.onRun() + } } }() + + return drt } func (drt *DelayedRepeatingTask) onRun() { drt.action() } -func NewRepeatingTask(seconds int64, action func()) { +func (drt *DelayedRepeatingTask) Stop() { + close(drt.stop) +} + +func NewRepeatingTask(seconds int64, action func()) *RepeatingTask { drt := &RepeatingTask{ Seconds: seconds, action: action, + stop: make(chan struct{}), } go func() { for { - drt.onRun() - time.Sleep(time.Duration(drt.Seconds) * time.Second) + select { + case <-drt.stop: + return + default: + drt.onRun() + time.Sleep(time.Duration(drt.Seconds) * time.Second) + } } }() + + return drt } func (drt *RepeatingTask) onRun() { drt.action() } + +func (drt *RepeatingTask) Stop() { + close(drt.stop) +} diff --git a/utils/utils.go b/utils/utils.go index 6fccbaf..9b996e2 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,11 +1,19 @@ package utils import ( + "bytes" + "encoding/json" "fmt" + "io" "math" + "net/http" "strconv" "strings" + "sync" "time" + + "github.com/HyPE-Network/vanilla-proxy/log" + "github.com/tailscale/hujson" ) func Format(a []any) string { @@ -57,3 +65,296 @@ func TimestampToDate(t int64) string { func FormatTime(t time.Time) string { return t.Format("15:04:05 2006.01.02") // magic numbers https://go.dev/src/time/format.go } + +// FetchDatabase fetches data from the database and handles 429 rate-limiting errors with retries +func FetchDatabase[T any](tableName string) (map[string]T, error) { + dbConfig := ReadConfig().Database + uri := dbConfig.Host + "/api/database/" + dbConfig.Name + "/table/" + tableName + + log.Logger.Info("Fetching from database", "table", tableName, "uri", uri) + + client := &http.Client{ + Timeout: time.Second * 10, // Longer timeout for database operations + } + var resp *http.Response + var err error + + // Implement retry logic with exponential backoff + retryAttempts := 5 + for i := 0; i < retryAttempts; i++ { + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + log.Logger.Error("Failed to create new request", "error", err) + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("authorization", dbConfig.Key) + + resp, err = client.Do(req) + if err != nil { + log.Logger.Warn("Database connection error, retrying...", "error", err, "attempts_left", retryAttempts-i) + if i == retryAttempts-1 { + LogErrorToDiscord(err) + } + time.Sleep(time.Duration(1< 0 { + resp, err = client.Do(req) + if err != nil { + log.Logger.Warn("Xbox API connection error, retrying...", "error", err, "attempts_left", maxRetries) + maxRetries-- + time.Sleep(backoff) + backoff *= 2 + continue + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusTooManyRequests { + // Handle 429 error with retry after exponential backoff + maxRetries-- + log.Logger.Warn("Received 429 Too Many Requests, retrying...", "attempt", 5-maxRetries) + + retryAfter := resp.Header.Get("Retry-After") + if retryAfter != "" { + // If the server provides a "Retry-After" header, respect it + delay, _ := strconv.Atoi(retryAfter) + time.Sleep(time.Duration(delay) * time.Second) + } else { + // Exponential backoff if no "Retry-After" header is provided + time.Sleep(backoff) + backoff *= 2 + } + continue + } + + if resp.StatusCode != http.StatusOK { + err := fmt.Errorf("failed to fetch Xbox Icon Link, status code: %d", resp.StatusCode) + return "", err + } + + break + } + + if maxRetries <= 0 { + return "", fmt.Errorf("exceeded maximum retries after connection issues") + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response body: %w", err) + } + + var data struct { + ProfileUsers []struct { + Settings []struct { + Value string `json:"value"` + } `json:"settings"` + } `json:"profileUsers"` + } + + err = json.Unmarshal(body, &data) + if err != nil { + LogErrorToDiscord(err) + return "", fmt.Errorf("failed to unmarshal response: %w", err) + } + + if len(data.ProfileUsers) == 0 || len(data.ProfileUsers[0].Settings) == 0 { + return "", fmt.Errorf("profile picture URL not found") + } + + profilePictureUrl := data.ProfileUsers[0].Settings[0].Value + profilePictureUrls.Store(xuid, profilePictureUrl) // Cache the result + + return profilePictureUrl, nil +} + +// ParseCommentedJSON parses JSON with comments and returns a JSON byte slice. +func ParseCommentedJSON(b []byte) ([]byte, error) { + ast, err := hujson.Parse(b) + if err != nil { + return b, err + } + ast.Standardize() + return ast.Pack(), nil +}