├── .gitignore ├── LICENSE ├── README.md ├── common ├── copact.go ├── infchannel.go └── varint.go ├── config └── config.go ├── game ├── blocks.go ├── gameloop.go ├── helpers.go ├── items.go ├── player.go ├── ticker.go └── world.go ├── main.go ├── math ├── helpers.go ├── point.go ├── rect.go └── rtree.go ├── minecraft ├── biome │ ├── biomes.go │ └── data.go ├── block │ ├── block.go │ ├── data.go │ └── meta.go ├── chat.go ├── chunk │ ├── chunk.go │ ├── constants.go │ ├── palette.go │ └── section.go ├── entity │ ├── data.go │ ├── entity.go │ ├── living.go │ └── player.go ├── item │ ├── data.go │ └── item.go ├── nbt.go ├── proto │ ├── login │ │ └── packets.go │ ├── play │ │ ├── entity.go │ │ ├── menu.go │ │ ├── other.go │ │ ├── player.go │ │ └── world.go │ └── status │ │ └── packets.go ├── reader.go ├── types.go ├── world │ ├── generator │ │ └── flatgrass │ │ │ └── generator.go │ ├── lighting.go │ ├── provider │ │ └── nullprovider │ │ │ └── provider.go │ └── world.go └── writer.go ├── scripts ├── generate_biomes.py ├── generate_blocks.py ├── generate_entities.py ├── generate_items.py └── generate_meta.py └── server ├── handshaking └── handlers.go ├── login └── handlers.go ├── play ├── menu.go └── player.go ├── server.go ├── socket └── socket.go └── status └── handlers.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | cpuprofile 3 | memprofile 4 | mcserver 5 | mcserver.exe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Itay Almog 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minecraft 1.15.2 Server 2 | 3 | This is a minecraft server written from scratch in Go! 4 | 5 | The goal is to create a full SMP vanilla server which will hopefully not be as heavy as the official one... 6 | 7 | If you have any questions feel free to message me on discord `itay#2805` 8 | 9 | ## Features 10 | 11 | Currently the focus is getting creative mode only fully working with all the block updates and such, 12 | afterwards we can get into implementing proper gameplay features that are needed for survival. 13 | 14 | Once that is going we can get into implementing a proper anti-cheat into the game and so on, but that is 15 | far far away :) 16 | 17 | ## Basics 18 | * Chunk sync 19 | * Chunk download 20 | * Chunk updates (block placing/breaking) 21 | * Proper light calculations 22 | * Entity & Players sync 23 | * Proper player spawning with Player info 24 | * All movement (including crouching and sprinting) 25 | * Equipment (sends a fake item that looks the same to not reveal the real properties of the item) 26 | * Animation (hand swing) 27 | 28 | ## Special blocks 29 | ### Proper placement 30 | * Torch 31 | * Carved Pumpkin 32 | * Anvil 33 | * Glazed Terracotta 34 | * Loom 35 | * Stonecutter 36 | * Stairs (all kinds) 37 | * missing proper water and upwards placement -------------------------------------------------------------------------------- /common/copact.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type CompactArray struct { 4 | Values []int64 5 | BitsPerElement int 6 | Max int 7 | } 8 | 9 | func alignUp(x, alignTo int) int { 10 | return (x + (alignTo - 1)) &^ (alignTo - 1) 11 | } 12 | 13 | func CompactArrayLength(bits, count int) int { 14 | return alignUp(bits * count, 64) / 64 15 | } 16 | 17 | func NewCompactArray(bits, count int) *CompactArray { 18 | return &CompactArray{ 19 | Values: make([]int64, CompactArrayLength(bits, count)), 20 | BitsPerElement: bits, 21 | Max: (1 << bits) - 1, 22 | } 23 | } 24 | 25 | func (c *CompactArray) Set(index, value int) int { 26 | // calculate the different indexes 27 | bIndex := index * c.BitsPerElement 28 | sIndex := bIndex >> 0x06 29 | eIndex := (((index + 1) * c.BitsPerElement) - 1) >> 0x06 30 | uIndex := bIndex ^ (sIndex << 0x06) 31 | 32 | // take the prev value 33 | previousValue := int64(uint64(c.Values[sIndex]) >> uIndex) & int64(c.Max) 34 | 35 | // set the new value 36 | c.Values[sIndex] = c.Values[sIndex] & int64(^(c.Max << uIndex)) | int64((value&c.Max) << uIndex) 37 | 38 | // if we cross boundries then handle it 39 | if sIndex != eIndex { 40 | zIndex := 64 - uIndex 41 | pIndex := c.BitsPerElement - 1 42 | 43 | // prev value... 44 | previousValue |= (c.Values[eIndex] << zIndex) & int64(c.Max) 45 | 46 | // new value... 47 | c.Values[eIndex] = int64(((uint64(c.Values[eIndex]) >> pIndex) << pIndex) | uint64((value&c.Max) >> zIndex)) 48 | } 49 | 50 | return int(previousValue) 51 | } 52 | 53 | func (c *CompactArray) Get(index int) int { 54 | bIndex := index * c.BitsPerElement 55 | sIndex := bIndex >> 0x06 56 | eIndex := (((index + 1) * c.BitsPerElement) - 1) >> 0x06 57 | uIndex := bIndex ^ (sIndex << 0x06) 58 | 59 | if sIndex == eIndex { 60 | return int((uint64(c.Values[sIndex]) >> uIndex) & uint64(c.Max)) 61 | } 62 | 63 | zIndex := 64 - uIndex 64 | 65 | return int(uint64(c.Values[sIndex] >> uIndex) | uint64(c.Values[eIndex] << zIndex) & uint64(c.Max)) 66 | } 67 | -------------------------------------------------------------------------------- /common/infchannel.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "container/list" 4 | 5 | func MakeInfinite() (chan <-interface{}, <-chan interface{}) { 6 | in := make(chan interface{}) 7 | out := make(chan interface{}) 8 | 9 | go func() { 10 | inQueue := list.List{} 11 | 12 | outCh := func() chan interface{} { 13 | if inQueue.Len() == 0 { 14 | return nil 15 | } 16 | return out 17 | } 18 | 19 | curVal := func() interface{} { 20 | if inQueue.Len() == 0 { 21 | return nil 22 | } 23 | return inQueue.Front().Value 24 | } 25 | 26 | for inQueue.Len() > 0 || in != nil { 27 | select { 28 | case v, ok := <-in: 29 | if !ok { 30 | in = nil 31 | } else { 32 | inQueue.PushBack(v) 33 | } 34 | case outCh() <- curVal(): 35 | inQueue.Remove(inQueue.Front()) 36 | } 37 | } 38 | close(out) 39 | }() 40 | 41 | return in, out 42 | } 43 | 44 | -------------------------------------------------------------------------------- /common/varint.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | func VarintSize(val int32) int { 4 | v := uint16(val) 5 | 6 | // TODO: faster implementation for this 7 | count := 0 8 | for { 9 | v >>= 7 10 | count++ 11 | if v == 0 { 12 | break 13 | } 14 | } 15 | return count 16 | } 17 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "flag" 4 | 5 | var MaxViewDistance = flag.Int("max-view-distance", 32, "The max view distance allowed on the server") 6 | -------------------------------------------------------------------------------- /game/blocks.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/minecraft" 5 | "github.com/itay2805/mcserver/minecraft/block" 6 | "github.com/itay2805/mcserver/minecraft/item" 7 | ) 8 | 9 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 10 | // Helper functions for transforming special blocks 11 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 12 | 13 | // 14 | // Transform a torch, just place it on a wall if needed and don't 15 | // allow to place it in the air 16 | // 17 | func (p *Player) transformTorch(face minecraft.Face) (uint16, bool) { 18 | if face == minecraft.FaceBottom { 19 | // can't place on the ceiling 20 | return 0, false 21 | } else if face == minecraft.FaceTop { 22 | // just return the default torch 23 | return block.Torch.DefaultStateId, true 24 | } else { 25 | // turn the facing to an axis 26 | return block.WallTorch.MinStateId + uint16(face) - 2, true 27 | } 28 | } 29 | 30 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | 32 | // 33 | // Have the block just face to player, this assumes there is no other state 34 | // info other than a 4-way facing 35 | // 36 | func (p *Player) transformFaceToPlayer(blk *block.Block) uint16 { 37 | return blk.MinStateId + uint16(p.GetFacing().Invert()) - 2 38 | } 39 | 40 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 41 | 42 | // 43 | // Transform stairs 44 | // 45 | // This handles the half the block is on (based on both the cursor and the face 46 | // the player placed on) and the facing itself which is based on the player. 47 | // 48 | // The connections to other stairs are gonna be handled in the block update 49 | // for simplicity since in placement we don't have the sync yet. 50 | // 51 | // TODO: handle water logged, based on the block of where the block is 52 | // gonna be placed 53 | // 54 | func (p *Player) transformStairs(face minecraft.Face, blk *block.Block) uint16 { 55 | // NOTE: The shape is handled in the block update instead 56 | // of here, since it will allow us to update the stairs nearby 57 | meta := block.StairsMeta{ 58 | Facing: p.GetFacing(), 59 | Half: minecraft.FaceTop, 60 | Shape: minecraft.ShapeStraight, 61 | Waterlogged: false, 62 | } 63 | 64 | // if placing on ceiling then turn upside down 65 | if face == minecraft.FaceTop { 66 | meta.Half = minecraft.FaceBottom 67 | } 68 | 69 | // TODO: if placing with cursor on top half then FaceBottom 70 | 71 | // TODO: if placing inside water block then turn waterlogged 72 | 73 | return blk.MinStateId + meta.ToMeta() 74 | } 75 | 76 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 77 | 78 | // 79 | // Do furnace transformation, aka, rotate to where the player is facing 80 | // 81 | func (p *Player) transformFurnace(blk *block.Block) uint16 { 82 | meta := block.FurnaceMeta{ 83 | Facing: p.GetFacing(), 84 | Lit: false, 85 | } 86 | return blk.MinStateId + meta.ToMeta() 87 | } 88 | 89 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 90 | // The top level transform function 91 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 92 | 93 | // 94 | // Top level transformation function that turns an item to a block, this will also handle 95 | // item uses like flint&steel and buckets. 96 | // 97 | // The reason for that is that it is just easier to handle it on block placement rather than 98 | // item use as the game sends more info 99 | // 100 | func (p *Player) TransformItemToStateId(itm *item.Item, face minecraft.Face) (uint16, bool) { 101 | switch itm { 102 | // Either on ground or based on block placed on 103 | case item.Torch: return p.transformTorch(face) 104 | 105 | // Facing based on player look 106 | case item.CarvedPumpkin: return p.transformFaceToPlayer(block.CarvedPumpkin), true 107 | case item.JackOLantern: return p.transformFaceToPlayer(block.JackOLantern), true 108 | case item.Anvil: return p.transformFaceToPlayer(block.Anvil), true 109 | case item.ChippedAnvil: return p.transformFaceToPlayer(block.ChippedAnvil), true 110 | case item.DamagedAnvil: return p.transformFaceToPlayer(block.DamagedAnvil), true 111 | case item.WhiteGlazedTerracotta: return p.transformFaceToPlayer(block.WhiteGlazedTerracotta), true 112 | case item.OrangeGlazedTerracotta: return p.transformFaceToPlayer(block.OrangeGlazedTerracotta), true 113 | case item.MagentaGlazedTerracotta: return p.transformFaceToPlayer(block.MagentaGlazedTerracotta), true 114 | case item.LightBlueGlazedTerracotta: return p.transformFaceToPlayer(block.LightBlueGlazedTerracotta), true 115 | case item.YellowGlazedTerracotta: return p.transformFaceToPlayer(block.YellowGlazedTerracotta), true 116 | case item.LimeGlazedTerracotta: return p.transformFaceToPlayer(block.LimeGlazedTerracotta), true 117 | case item.PinkGlazedTerracotta: return p.transformFaceToPlayer(block.PinkGlazedTerracotta), true 118 | case item.GrayGlazedTerracotta: return p.transformFaceToPlayer(block.GrayGlazedTerracotta), true 119 | case item.LightGrayGlazedTerracotta: return p.transformFaceToPlayer(block.LightGrayGlazedTerracotta), true 120 | case item.CyanGlazedTerracotta: return p.transformFaceToPlayer(block.CyanGlazedTerracotta), true 121 | case item.PurpleGlazedTerracotta: return p.transformFaceToPlayer(block.PurpleGlazedTerracotta), true 122 | case item.BlueGlazedTerracotta: return p.transformFaceToPlayer(block.BlueGlazedTerracotta), true 123 | case item.BrownGlazedTerracotta: return p.transformFaceToPlayer(block.BrownGlazedTerracotta), true 124 | case item.GreenGlazedTerracotta: return p.transformFaceToPlayer(block.GreenGlazedTerracotta), true 125 | case item.RedGlazedTerracotta: return p.transformFaceToPlayer(block.RedGlazedTerracotta), true 126 | case item.BlackGlazedTerracotta: return p.transformFaceToPlayer(block.BlackGlazedTerracotta), true 127 | case item.Loom: return p.transformFaceToPlayer(block.Loom), true 128 | case item.Stonecutter: return p.transformFaceToPlayer(block.Stonecutter), true 129 | 130 | // handle stairs 131 | case item.OakStairs: return p.transformStairs(face, block.OakStairs), true 132 | case item.CobblestoneStairs: return p.transformStairs(face, block.CobblestoneStairs), true 133 | case item.BrickStairs: return p.transformStairs(face, block.BrickStairs), true 134 | case item.StoneBrickStairs: return p.transformStairs(face, block.StoneBrickStairs), true 135 | case item.NetherBrickStairs: return p.transformStairs(face, block.NetherBrickStairs), true 136 | case item.SandstoneStairs: return p.transformStairs(face, block.SandstoneStairs), true 137 | case item.SpruceStairs: return p.transformStairs(face, block.SpruceStairs), true 138 | case item.BirchStairs: return p.transformStairs(face, block.BirchStairs), true 139 | case item.JungleStairs: return p.transformStairs(face, block.JungleStairs), true 140 | case item.QuartzStairs: return p.transformStairs(face, block.QuartzStairs), true 141 | case item.AcaciaStairs: return p.transformStairs(face, block.AcaciaStairs), true 142 | case item.DarkOakStairs: return p.transformStairs(face, block.DarkOakStairs), true 143 | case item.PrismarineStairs: return p.transformStairs(face, block.PrismarineStairs), true 144 | case item.PrismarineBrickStairs: return p.transformStairs(face, block.PrismarineBrickStairs), true 145 | case item.DarkPrismarineStairs: return p.transformStairs(face, block.DarkPrismarineStairs), true 146 | case item.RedSandstoneStairs: return p.transformStairs(face, block.RedSandstoneStairs), true 147 | case item.PurpurStairs: return p.transformStairs(face, block.PurpurStairs), true 148 | case item.PolishedGraniteStairs: return p.transformStairs(face, block.PolishedGraniteStairs), true 149 | case item.SmoothRedSandstoneStairs: return p.transformStairs(face, block.SmoothRedSandstoneStairs), true 150 | case item.MossyStoneBrickStairs: return p.transformStairs(face, block.MossyStoneBrickStairs), true 151 | case item.PolishedDioriteStairs: return p.transformStairs(face, block.PolishedDioriteStairs), true 152 | case item.MossyCobblestoneStairs: return p.transformStairs(face, block.MossyCobblestoneStairs), true 153 | case item.EndStoneBrickStairs: return p.transformStairs(face, block.EndStoneBrickStairs), true 154 | case item.StoneStairs: return p.transformStairs(face, block.StoneStairs), true 155 | case item.SmoothSandstoneStairs: return p.transformStairs(face, block.SmoothSandstoneStairs), true 156 | case item.SmoothQuartzStairs: return p.transformStairs(face, block.SmoothQuartzStairs), true 157 | case item.GraniteStairs: return p.transformStairs(face, block.GraniteStairs), true 158 | case item.AndesiteStairs: return p.transformStairs(face, block.AndesiteStairs), true 159 | case item.RedNetherBrickStairs: return p.transformStairs(face, block.RedNetherBrickStairs), true 160 | case item.PolishedAndesiteStairs: return p.transformStairs(face, block.PolishedAndesiteStairs), true 161 | case item.DioriteStairs: return p.transformStairs(face, block.DioriteStairs), true 162 | 163 | // handle furnace 164 | case item.Furnace: return p.transformFurnace(block.Furnace), true 165 | case item.BlastFurnace: return p.transformFurnace(block.BlastFurnace), true 166 | 167 | default: 168 | // by default just turn the item into the block it represents 169 | // and use its default state 170 | blk := block.FromItem(itm) 171 | if blk == nil { 172 | return 0, false 173 | } else { 174 | return blk.DefaultStateId, true 175 | } 176 | } 177 | } 178 | 179 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 180 | // Block updates 181 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 182 | 183 | // 184 | // This is done right after the chunk processes all of the block changes that happened in this tick 185 | // the reason for this is to make sure the map is in the most updated state. 186 | // 187 | // if you want to register future block updates just use the ticker to register a block update, and 188 | // since right after the ticker the block updates happen it will just work 189 | // 190 | func BlockUpdate(position minecraft.Position, blk block.Block) { 191 | } 192 | -------------------------------------------------------------------------------- /game/gameloop.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/itay2805/mcserver/minecraft/world" 6 | "github.com/itay2805/mcserver/minecraft/world/generator/flatgrass" 7 | "github.com/itay2805/mcserver/minecraft/world/provider/nullprovider" 8 | "log" 9 | "runtime" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | ) 14 | 15 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 16 | // Game loop api 17 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 18 | 19 | // list of all players 20 | var players = make(map[uuid.UUID]*Player) 21 | var playerCount int32 22 | 23 | var newPlayers []*Player 24 | var newPlayersMutex sync.Mutex 25 | 26 | var leftPlayers []*Player 27 | var leftPlayersMutex sync.Mutex 28 | 29 | func GetPlayerCount() int32 { 30 | return atomic.LoadInt32(&playerCount) 31 | } 32 | 33 | // 34 | // Add a player to the server 35 | // 36 | func JoinPlayer(player *Player) { 37 | newPlayersMutex.Lock() 38 | defer newPlayersMutex.Unlock() 39 | newPlayers = append(newPlayers, player) 40 | } 41 | 42 | func LeftPlayer(player *Player) { 43 | leftPlayersMutex.Lock() 44 | defer leftPlayersMutex.Unlock() 45 | leftPlayers = append(leftPlayers, player) 46 | } 47 | 48 | // the world we are going to use 49 | var OurWorld = NewWorld( 50 | &flatgrass.FlatgraassGenerator{}, 51 | &nullprovider.NullProvider{}, 52 | ) 53 | 54 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 55 | // Game loop sync stages 56 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 57 | 58 | func syncServerState() { 59 | // add all new players to the world 60 | for _, p := range newPlayers { 61 | OurWorld.AddPlayer(p) 62 | players[p.UUID] = p 63 | playerCount++ 64 | } 65 | 66 | // remove any players that have left from 67 | // the world 68 | for _, p := range leftPlayers { 69 | delete(players, p.UUID) 70 | p.World.RemovePlayer(p) 71 | playerCount-- 72 | } 73 | 74 | // sync any changes made by the clients in 75 | // the last tick 76 | for _, p := range players { 77 | p.syncChanges() 78 | } 79 | } 80 | 81 | func syncClientsState() { 82 | for _, p := range players { 83 | p.syncClient() 84 | } 85 | } 86 | 87 | func cleanupTick() { 88 | for _, p := range players { 89 | p.cleanupTick() 90 | } 91 | 92 | OurWorld.cleanup() 93 | 94 | // finished updating players 95 | // for this tick 96 | newPlayers = nil 97 | leftPlayers = nil 98 | } 99 | 100 | func tickObjects() { 101 | // all the players, will process their actions and whatever they did on the world 102 | for _, p := range players { 103 | p.tick() 104 | } 105 | } 106 | 107 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 108 | // The game loop 109 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 110 | 111 | func StartGameLoop() { 112 | log.Println("Starting gameloop") 113 | 114 | // create another thread to compensate for the one we are using now 115 | runtime.GOMAXPROCS(runtime.GOMAXPROCS(runtime.NumCPU() + 1)) 116 | runtime.LockOSThread() 117 | 118 | ticker := time.NewTicker(time.Second / 20) 119 | lastLog := time.Now() 120 | ticks := 0 121 | for range ticker.C { 122 | // any thing that can't be changed while we are 123 | // doing a game tick need to be locked here 124 | leftPlayersMutex.Lock() 125 | newPlayersMutex.Lock() 126 | 127 | // start by syncing all of the states, this is required 128 | // to process any change sent out from players that we 129 | // want to know about this tick 130 | syncServerState() 131 | 132 | // process all of the objects that need to be ticked 133 | // at all time 134 | tickObjects() 135 | 136 | // tick all the objects that have been scheduled for a tick on 137 | // this tick 138 | tickScheduledObjects() 139 | 140 | // once all the ticks have been passed we can modify all the 141 | // changes that happened to the world 142 | // TODO: for each world 143 | OurWorld.syncState() 144 | 145 | // once the world has been synced we are going to process all 146 | // of the light updates that have happened this tick 147 | world.ProcessLightUpdates() 148 | 149 | // finally sync all of the clients states, this is called 150 | // after the server did all of its processing 151 | syncClientsState() 152 | 153 | // Do any last cleanups before we are finishing up with 154 | // this tick 155 | cleanupTick() 156 | 157 | // increment the game ticks 158 | ticks++ 159 | 160 | // log the tps, just for sanity 161 | if time.Since(lastLog) > time.Second { 162 | log.Printf("%d tp%s", 163 | ticks, 164 | time.Since(lastLog).Round(time.Second)) 165 | ticks = 0 166 | lastLog = time.Now() 167 | } 168 | 169 | // and released here 170 | newPlayersMutex.Unlock() 171 | leftPlayersMutex.Unlock() 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /game/helpers.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | func forEachChunkInRange(x0, y0, radius int, cb func(x, z int)) { 4 | for x := x0 - radius - 2; x < x0 + radius + 2; x++ { 5 | for z := y0 - radius - 2; z < y0 + radius + 2; z++ { 6 | cb(x, z) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /game/items.go: -------------------------------------------------------------------------------- 1 | package game 2 | -------------------------------------------------------------------------------- /game/player.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/itay2805/mcserver/math" 6 | "github.com/itay2805/mcserver/minecraft" 7 | "github.com/itay2805/mcserver/minecraft/entity" 8 | "github.com/itay2805/mcserver/minecraft/item" 9 | "github.com/itay2805/mcserver/minecraft/proto/play" 10 | "github.com/itay2805/mcserver/minecraft/world" 11 | "github.com/itay2805/mcserver/server/socket" 12 | "log" 13 | math2 "math" 14 | "reflect" 15 | "sync" 16 | "time" 17 | 18 | "github.com/eapache/queue" 19 | ) 20 | 21 | type PendingChange struct { 22 | Field interface{} 23 | Value interface{} 24 | ChangeFlag interface{} 25 | } 26 | 27 | type PlayerActionType int 28 | 29 | const ( 30 | PlayerActionDig = PlayerActionType(iota) 31 | PlayerActionPlace 32 | PlayerActionUseItem 33 | ) 34 | 35 | type BlockPlacement struct { 36 | Hand int32 37 | Location minecraft.Position 38 | Face minecraft.Face 39 | CursorPositionX float32 40 | CursorPositionY float32 41 | CursorPositionZ float32 42 | } 43 | 44 | type PlayerAction struct { 45 | Type PlayerActionType 46 | Data interface{} 47 | } 48 | 49 | type Player struct { 50 | // the socket 51 | socket.Socket 52 | 53 | // The entity player 54 | *entity.Player 55 | 56 | // Client settings we need to know about 57 | ViewDistance int 58 | 59 | // pending changes 60 | changeQueue *queue.Queue 61 | changeMutex sync.Mutex 62 | 63 | // ping related 64 | Ping time.Duration 65 | LastKeepAlive time.Time 66 | PingChanged bool 67 | 68 | // The world we are in 69 | World *World 70 | 71 | // the chunks loaded by the client of this player 72 | loadedChunks map[world.ChunkPos]bool 73 | loadedEntities map[int32]bool 74 | 75 | // player info 76 | waitingForPlayers map[uuid.UUID]chan bool 77 | knownPlayers map[uuid.UUID]bool 78 | joined bool 79 | 80 | // sound related 81 | lastSoundPos math.Point 82 | lastSoundTime time.Time 83 | 84 | ActionQueue *queue.Queue 85 | 86 | // the inventory of the player 87 | // https://wiki.vg/images/1/13/Inventory-slots.png 88 | HeldItemIndex int 89 | Inventory [46]*play.Slot 90 | } 91 | 92 | func (p *Player) String() string { 93 | if p.Player != nil { 94 | return p.Username 95 | } else { 96 | return p.RemoteAddr().String() 97 | } 98 | } 99 | 100 | func (p *Player) UnitVector() math.Point { 101 | pitch := p.Pitch.ToRadians() 102 | yaw := p.Yaw.ToRadians() 103 | return math.NewPoint( 104 | -math2.Cos(pitch) * math2.Sin(yaw), 105 | -math2.Cos(pitch), 106 | -math2.Cos(pitch) * math2.Cos(yaw), 107 | ) 108 | } 109 | func NewPlayer(socket socket.Socket) *Player { 110 | return &Player{ 111 | Socket: socket, 112 | Player: nil, 113 | 114 | ViewDistance: 2, 115 | 116 | changeQueue: queue.New(), 117 | changeMutex: sync.Mutex{}, 118 | 119 | Ping: -1, 120 | LastKeepAlive: time.Now(), 121 | PingChanged: false, 122 | 123 | World: nil, 124 | 125 | loadedChunks: make(map[world.ChunkPos]bool), 126 | loadedEntities: make(map[int32]bool), 127 | 128 | waitingForPlayers: make(map[uuid.UUID]chan bool), 129 | knownPlayers: make(map[uuid.UUID]bool), 130 | joined: true, 131 | 132 | ActionQueue: queue.New(), 133 | 134 | HeldItemIndex: 36, 135 | } 136 | } 137 | 138 | func (p *Player) ChangeHeldItem(slot int) { 139 | // change the index of the held item 140 | p.HeldItemIndex = slot + 36 141 | p.Equipment[0] = p.Inventory[p.HeldItemIndex] 142 | p.EquipmentChanged |= 1 143 | } 144 | 145 | // this is used when there is data from the player that should 146 | // be updated on the server, this will also make sure to give 147 | // back the player a confirmation about this 148 | func (p *Player) UpdateInventory(slot int, item *play.Slot) { 149 | // set it 150 | p.Inventory[slot] = item 151 | 152 | // check if need to update the equipment of the 153 | // entity 154 | index := -1 155 | 156 | switch slot { 157 | case p.HeldItemIndex: 158 | index = 0 159 | case 45: 160 | index = 1 161 | case 8: 162 | index = 2 163 | case 7: 164 | index = 3 165 | case 6: 166 | index = 4 167 | case 5: 168 | index = 5 169 | 170 | } 171 | 172 | if index != -1 { 173 | p.Equipment[index] = p.Inventory[slot] 174 | p.EquipmentChanged |= 1 << index 175 | } 176 | 177 | // if not the down slots and this is a remove 178 | // item then send back a set no item, this is 179 | // needed when clearing inventory in creative 180 | if item == nil && !(36 <= slot && slot <= 44) { 181 | p.Send(play.SetSlot{ 182 | WindowID: -2, 183 | Slot: int16(slot), 184 | SlotData: nil, 185 | }) 186 | } 187 | } 188 | 189 | func (p *Player) Change(cb func()) { 190 | p.changeMutex.Lock() 191 | defer p.changeMutex.Unlock() 192 | p.changeQueue.Add(cb) 193 | } 194 | 195 | // 196 | // Get a rect with the area that a player can see 197 | // 198 | func (p *Player) ViewRect() *math.Rect { 199 | return math.NewRectFromPoints( 200 | math.NewPoint( 201 | p.Position.X() - float64(p.ViewDistance * 16), 202 | 0, 203 | p.Position.Z() - float64(p.ViewDistance * 16), 204 | ), 205 | math.NewPoint( 206 | p.Position.X() + float64(p.ViewDistance * 16), 207 | 256, 208 | p.Position.Z() + float64(p.ViewDistance * 16), 209 | ), 210 | ) 211 | } 212 | 213 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 214 | // Sync server 215 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 216 | 217 | func (p *Player) syncChanges() { 218 | // apply all changes 219 | p.changeMutex.Lock() 220 | for p.changeQueue.Length() > 0 { 221 | p.changeQueue.Remove().(func())() 222 | } 223 | p.changeMutex.Unlock() 224 | 225 | // if moved update the entity 226 | // position and bounding box 227 | if p.Moved { 228 | p.World.UpdateEntityPosition(p) 229 | } 230 | } 231 | 232 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 233 | // Tick player 234 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 235 | 236 | func (p *Player) tickDig(position minecraft.Position) { 237 | // TODO: don't assume creative 238 | 239 | // Swing main hand because we are digging 240 | p.Animation = play.AnimationSwingMainHand 241 | 242 | // get the prev block 243 | blkstate := p.World.GetBlockState(position.X, position.Y, position.Z) 244 | 245 | // register a block change 246 | chunkX := position.X >> 4 247 | chunkZ := position.Z >> 4 248 | pos := world.ChunkPos{ X: chunkX, Z: chunkZ } 249 | p.World.BlockChanges[pos] = append(p.World.BlockChanges[pos], play.BlockRecord{ 250 | BlockX: byte(position.X & 0xf), 251 | BlockZ: byte(position.Z & 0xf), 252 | BlockY: byte(position.Y), 253 | BlockState: 0, 254 | }) 255 | 256 | // send the sound and animation to other players which are in a 16 block range from 257 | // the given effect, farther than that they will just not be able to see it 258 | soundDistance := math.NewRect(position.ToPoint().SubScalar(16), [3]float64{ 32, 32, 32 }) 259 | p.World.ForEntitiesInRange(soundDistance, func(ient entity.IEntity) { 260 | // ignore local player, their client does this anyways 261 | if ient == p { 262 | return 263 | } 264 | 265 | // ignore non-players 266 | if other, ok := ient.(*Player); ok { 267 | other.Send(play.Effect{ 268 | EffectID: play.EffectBlockBreak, 269 | Location: position, 270 | Data: int32(blkstate), 271 | DisableRelativeVolume: false, 272 | }) 273 | } else { 274 | log.Println("Not sending to", ient, reflect.TypeOf(ient)) 275 | } 276 | }) 277 | } 278 | 279 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 280 | 281 | func (p *Player) tickPlace(placement BlockPlacement) { 282 | // get the action position 283 | blockPos := placement.Location.ApplyFace(placement.Face) 284 | 285 | // Get the item 286 | var slot *play.Slot 287 | if placement.Hand == 0 { 288 | slot = p.Inventory[p.HeldItemIndex] 289 | } else { 290 | slot = p.Inventory[45] 291 | } 292 | 293 | // nothing in the hand 294 | if slot == nil { 295 | return 296 | } 297 | 298 | itm := item.GetById(int(slot.ItemID)) 299 | 300 | // turn the item into a block that we need to place 301 | blkStateId, ok := p.TransformItemToStateId(itm, placement.Face) 302 | if ok { 303 | // now that we know we can place the block set 304 | // the animation 305 | if placement.Hand == 0 { 306 | p.Animation = play.AnimationSwingMainHand 307 | } else { 308 | p.Animation = play.AnimationSwingOffhand 309 | } 310 | 311 | // register a block change 312 | chunkX := blockPos.X >> 4 313 | chunkZ := blockPos.Z >> 4 314 | pos := world.ChunkPos{ X: chunkX, Z: chunkZ } 315 | p.World.BlockChanges[pos] = append(p.World.BlockChanges[pos], play.BlockRecord{ 316 | BlockX: byte(blockPos.X & 0xf), 317 | BlockZ: byte(blockPos.Z & 0xf), 318 | BlockY: byte(blockPos.Y), 319 | BlockState: blkStateId, 320 | }) 321 | 322 | // TODO: block updates 323 | // TODO: block placement sound (kinda complicated...) 324 | } 325 | } 326 | 327 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 328 | 329 | func (p *Player) tickUseItem(hand int32) { 330 | // Get the item 331 | var slot *play.Slot 332 | if hand == 0 { 333 | slot = p.Inventory[p.HeldItemIndex] 334 | } else { 335 | slot = p.Inventory[45] 336 | } 337 | 338 | // nothing in the hand 339 | if slot == nil { 340 | return 341 | } 342 | 343 | itm := item.GetById(int(slot.ItemID)) 344 | switch itm { 345 | } 346 | } 347 | 348 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 349 | 350 | func (p *Player) tick() { 351 | for p.ActionQueue.Length() > 0 { 352 | action := p.ActionQueue.Remove().(PlayerAction) 353 | switch action.Type { 354 | case PlayerActionDig: 355 | p.tickDig(action.Data.(minecraft.Position)) 356 | case PlayerActionPlace: 357 | p.tickPlace(action.Data.(BlockPlacement)) 358 | case PlayerActionUseItem: 359 | p.tickUseItem(action.Data.(int32)) 360 | } 361 | } 362 | } 363 | 364 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 365 | // Sync client 366 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 367 | 368 | // 369 | // Sync the player infos, needed before we can send 370 | // 371 | func (p *Player) syncPlayerInfo() { 372 | var pladd []play.PIAddPlayer 373 | var plrem []play.PIRemovePlayer 374 | var pllat []play.PIUpdateLatency 375 | 376 | // check for any players that we already got 377 | for uid, c := range p.waitingForPlayers { 378 | // check about this channel 379 | select { 380 | case _, ok := <-c: 381 | if ok { 382 | close(c) 383 | } 384 | 385 | delete(p.waitingForPlayers, uid) 386 | p.knownPlayers[uid] = true 387 | default: 388 | // not sent yet, ignore 389 | } 390 | } 391 | 392 | // check for any new players 393 | if p.joined { 394 | pladd = make([]play.PIAddPlayer, 0, len(players)) 395 | for _, pNew := range players { 396 | pladd = append(pladd, play.PIAddPlayer{ 397 | UUID: pNew.UUID, 398 | Name: pNew.Username, 399 | Gamemode: 0, 400 | Ping: int32(pNew.Ping.Milliseconds()), 401 | }) 402 | } 403 | } else { 404 | // the player has a list of everyone, only send updates 405 | pladd = make([]play.PIAddPlayer, 0, len(newPlayers)) 406 | plrem = make([]play.PIRemovePlayer, 0, len(leftPlayers)) 407 | 408 | for _, pNew := range newPlayers { 409 | pladd = append(pladd, play.PIAddPlayer{ 410 | UUID: pNew.UUID, 411 | Name: pNew.Username, 412 | Gamemode: 0, 413 | Ping: int32(pNew.Ping.Milliseconds()), 414 | }) 415 | } 416 | 417 | for _, pNew := range leftPlayers { 418 | plrem = append(plrem, play.PIRemovePlayer{UUID: pNew.UUID}) 419 | } 420 | } 421 | 422 | // update latencies 423 | for _, pNew := range players { 424 | if p.PingChanged { 425 | pllat = append(pllat, play.PIUpdateLatency{ 426 | UUID: pNew.UUID, 427 | Ping: int32(pNew.Ping.Milliseconds()), 428 | }) 429 | } 430 | } 431 | 432 | if len(pladd) > 0 { 433 | // insert all the players we are waiting for to 434 | // the map 435 | done := make(chan bool) 436 | p.SendChan(play.PlayerInfo{ AddPlayer: pladd }, done) 437 | for _, pNew := range pladd { 438 | p.waitingForPlayers[pNew.UUID] = done 439 | } 440 | } 441 | 442 | if len(plrem) > 0 { 443 | p.Send(play.PlayerInfo{ RemovePlayer: plrem }) 444 | } 445 | 446 | if len(pllat) > 0 { 447 | p.Send(play.PlayerInfo{ UpdateLatency: pllat }) 448 | } 449 | } 450 | 451 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 452 | 453 | func (p *Player) syncChunks() { 454 | // first set all the chunks as uneeded 455 | for pos := range p.loadedChunks { 456 | p.loadedChunks[pos] = false 457 | } 458 | 459 | // go over all the chunks in the view distance 460 | forEachChunkInRange(int(p.Position.X()) >> 4, int(p.Position.Z()) >> 4, p.ViewDistance, func(x, z int) { 461 | pos := world.ChunkPos{X: x, Z: z} 462 | 463 | if _, ok := p.loadedChunks[pos]; !ok { 464 | // this chunk is not loaded at the player 465 | // load the chunk in an async manner 466 | go p.World.SendChunkToPlayer(x, z, p) 467 | 468 | } else { 469 | changes := p.World.BlockChanges[pos] 470 | if len(changes) > 0 { 471 | if len(changes) > 1 { 472 | // multi-block change 473 | p.Send(play.MultiBlockChange{ 474 | ChunkX: int32(x), 475 | ChunkZ: int32(z), 476 | Records: changes, 477 | }) 478 | } else { 479 | // single block change 480 | p.Send(play.BlockChange{ 481 | Location: minecraft.Position{ 482 | X: (x << 4) | int(changes[0].BlockX), 483 | Y: int(changes[0].BlockY), 484 | Z: (z << 4) | int(changes[0].BlockZ), 485 | }, 486 | BlockID: changes[0].BlockState, 487 | }) 488 | } 489 | } 490 | } 491 | 492 | // mark this chunk as needed 493 | p.loadedChunks[pos] = true 494 | }) 495 | 496 | // now any chunk that is still loaded client side 497 | // and is not needed send an unload packet 498 | for pos, val := range p.loadedChunks { 499 | if !val { 500 | delete(p.loadedChunks, pos) 501 | 502 | p.Send(play.UnloadChunk{ 503 | ChunkX: int32(pos.X), 504 | ChunkZ: int32(pos.Z), 505 | }) 506 | } 507 | } 508 | } 509 | 510 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 511 | 512 | func (p *Player) syncEntities() { 513 | // set all entities as not needed 514 | for eid := range p.loadedEntities { 515 | p.loadedEntities[eid] = false 516 | } 517 | 518 | // go over the entities and tick them 519 | p.World.ForEntitiesInRange(p.ViewRect(), func(ient entity.IEntity) { 520 | // skip current player 521 | if ient == p { 522 | return 523 | } 524 | 525 | // get the entity data 526 | e := ient.GetEntity() 527 | newEntity := false 528 | 529 | // first check if the entity is known or not 530 | if _, ok := p.loadedEntities[e.EID]; ok { 531 | 532 | // this entity is known, tick its position if needed 533 | if e.Moved && e.Rotated { 534 | p.Send(play.EntityTeleport{ 535 | EntityID: e.EID, 536 | X: e.Position.X(), 537 | Y: e.Position.Y(), 538 | Z: e.Position.Z(), 539 | Yaw: e.Yaw, 540 | Pitch: e.Pitch, 541 | OnGround: e.OnGround, 542 | }) 543 | 544 | p.Send(play.EntityHeadLook{ 545 | EntityID: e.EID, 546 | HeadYaw: e.HeadYaw, 547 | }) 548 | 549 | } else if e.Moved { 550 | 551 | p.Send(play.EntityTeleport{ 552 | EntityID: e.EID, 553 | X: e.Position.X(), 554 | Y: e.Position.Y(), 555 | Z: e.Position.Z(), 556 | Yaw: e.Yaw, 557 | Pitch: e.Pitch, 558 | OnGround: e.OnGround, 559 | }) 560 | 561 | } else if e.Rotated { 562 | p.Send(play.EntityRotation{ 563 | EntityID: e.EID, 564 | Yaw: e.Yaw, 565 | Pitch: e.Pitch, 566 | OnGround: e.OnGround, 567 | }) 568 | p.Send(play.EntityHeadLook{ 569 | EntityID: e.EID, 570 | HeadYaw: e.HeadYaw, 571 | }) 572 | } else { 573 | // nothing happend, just keep reminding the player about 574 | // this entity 575 | // TODO: do I really need to do this? 576 | p.Send(play.EntityMovement{ 577 | EntityID: e.EID, 578 | }) 579 | } 580 | 581 | // TODO: handle other entity specific stuff 582 | 583 | } else { 584 | newEntity = true 585 | 586 | // this entity is unknown, spawn it 587 | switch other := ient.(type) { 588 | case *Player: 589 | // check the client has the player info 590 | // about this player before sending it 591 | if _, ok := p.knownPlayers[other.UUID]; !ok { 592 | return 593 | } 594 | 595 | // This is a player, spawn it 596 | p.Send(play.SpawnPlayer{ 597 | EntityID: other.EID, 598 | UUID: other.UUID, 599 | X: other.Position.X(), 600 | Y: other.Position.Y(), 601 | Z: other.Position.Z(), 602 | Yaw: other.Yaw, 603 | Pitch: other.Pitch, 604 | }) 605 | 606 | default: 607 | // This is a mob 608 | } 609 | } 610 | 611 | // entity animation 612 | if e.Animation != play.AnimationNone { 613 | p.Send(play.EntityAnimation{ 614 | EntityID: e.EID, 615 | Animation: e.Animation, 616 | }) 617 | } 618 | 619 | // check if there is equipment to update 620 | if newEntity || e.EquipmentChanged != 0 { 621 | for i, eq := range e.Equipment { 622 | if (e.EquipmentChanged & (1 << i)) == 0 { 623 | continue 624 | } 625 | 626 | // send a fake item that looks the same 627 | p.Send(play.EntityEquipment{ 628 | EntityID: e.EID, 629 | Slot: int32(i), 630 | Item: eq.CreateFake(), 631 | }) 632 | } 633 | } 634 | 635 | // check if there is metadata to update 636 | if newEntity || e.MetadataChanged { 637 | p.Send(play.EntityMetadata{ 638 | EntityID: e.EID, 639 | Metadata: e, 640 | }) 641 | } 642 | 643 | // set entity as known 644 | p.loadedEntities[e.EID] = true 645 | }) 646 | 647 | // unload all uneeded entities 648 | ids := make([]int32, 0) 649 | for eid, val := range p.loadedEntities { 650 | if !val { 651 | delete(p.loadedEntities, eid) 652 | ids = append(ids, eid) 653 | } 654 | } 655 | 656 | // if there are entities to unload, unload them 657 | if len(ids) > 0 { 658 | p.Send(play.DestroyEntities{EntityIDs: ids}) 659 | } 660 | } 661 | 662 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 663 | 664 | // 665 | // sync the client state 666 | // 667 | func (p *Player) syncClient() { 668 | p.syncPlayerInfo() 669 | p.syncChunks() 670 | p.syncEntities() 671 | 672 | // send keepalive if needed 673 | if time.Since(p.LastKeepAlive) > time.Second * 25 { 674 | now := time.Now() 675 | p.LastKeepAlive = now 676 | p.Send(play.KeepAlive{ KeepAliveId: now.UnixNano() }) 677 | } 678 | } 679 | 680 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 681 | // Cleanup 682 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 683 | 684 | func (p *Player) cleanupTick() { 685 | p.Moved = false 686 | p.Rotated = false 687 | p.MetadataChanged = false 688 | p.Animation = play.AnimationNone 689 | p.joined = false 690 | } -------------------------------------------------------------------------------- /game/ticker.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "container/list" 5 | ) 6 | 7 | type tickObject struct { 8 | targetTick int 9 | callback func() 10 | } 11 | 12 | // 13 | // The current tick we are processing 14 | // 15 | var currentTick = 0 16 | 17 | // 18 | // The objects that need to be ticked 19 | // 20 | var tickedObjects = list.New() 21 | 22 | // 23 | // Inserts a new ticked object to the list, finds 24 | // a place where the tick need to be placed 25 | // 26 | func insertTickObject(el tickObject) { 27 | for e := tickedObjects.Front(); e != nil; e = e.Next() { 28 | val := e.Value.(tickObject) 29 | if val.targetTick > el.targetTick { 30 | tickedObjects.InsertBefore(el, e) 31 | return 32 | } 33 | } 34 | tickedObjects.PushBack(el) 35 | } 36 | 37 | // 38 | // Tick all the objects that need to be ticked 39 | // on this tick 40 | // 41 | func tickScheduledObjects() { 42 | var next *list.Element 43 | for e := tickedObjects.Front(); e != nil; e = next { 44 | next = e.Next() 45 | val := e.Value.(tickObject) 46 | 47 | if val.targetTick == currentTick { 48 | val.callback() 49 | tickedObjects.Remove(e) 50 | } else { 51 | break 52 | } 53 | } 54 | } 55 | 56 | func RegisterForTick(cb func(), ticks int) { 57 | insertTickObject(tickObject{ 58 | targetTick: currentTick + ticks, 59 | callback: cb, 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /game/world.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/config" 5 | "github.com/itay2805/mcserver/math" 6 | "github.com/itay2805/mcserver/minecraft" 7 | "github.com/itay2805/mcserver/minecraft/entity" 8 | "github.com/itay2805/mcserver/minecraft/proto/play" 9 | "github.com/itay2805/mcserver/minecraft/world" 10 | "sync/atomic" 11 | ) 12 | 13 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 14 | // EID generration 15 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | var eidGen int32 18 | 19 | // Generate a new entity id for this session 20 | func generateEntityId() int32 { 21 | return atomic.AddInt32(&eidGen, 1) 22 | } 23 | 24 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 25 | // World related 26 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | type World struct { 29 | // the minecraft world 30 | *world.World 31 | 32 | // The block changes in this world 33 | BlockChanges map[world.ChunkPos][]play.BlockRecord 34 | 35 | // the entities 36 | entities *math.Rtree 37 | } 38 | 39 | func NewWorld(generator world.Generator, provider world.Provider) *World { 40 | return &World{ 41 | World: world.NewWorld(provider, generator), 42 | entities: math.NewRTree(10, 2000), 43 | BlockChanges: make(map[world.ChunkPos][]play.BlockRecord), 44 | } 45 | } 46 | 47 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 48 | // Entity mangaement 49 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 50 | 51 | func (w *World) AddPlayer(player *Player) { 52 | player.EID = generateEntityId() 53 | player.World = w 54 | 55 | // TODO: load from provider 56 | 57 | // set the spawn point 58 | pos := w.World.Generator.GetSpawn() 59 | player.Position = math.NewPoint(float64(pos.X), float64(pos.Y), float64(pos.Z)) 60 | player.UpdateBounds() 61 | 62 | // insert the player to the entity list 63 | w.entities.Insert(player) 64 | 65 | // send the join game 66 | player.Send(play.JoinGame{ 67 | EntityId: player.EID, 68 | Gamemode: 1, 69 | Dimension: 0, 70 | HashedSeed: 0, 71 | LevelType: "default", 72 | ViewDistance: int32(*config.MaxViewDistance), 73 | ReducedDebugInfo: false, 74 | EnableRespawnScreen: true, 75 | }) 76 | 77 | // send the player coords 78 | // TODO: send respawn packet if need to change world 79 | player.Send(play.PlayerPositionAndLook{ 80 | X: player.Position.X(), 81 | Y: player.Position.Y(), 82 | Z: player.Position.Z(), 83 | Yaw: 0, 84 | Pitch: 0, 85 | Flags: 0, 86 | TeleportId: 69, 87 | }) 88 | } 89 | 90 | func (w *World) UpdateEntityPosition(entity entity.IEntity) { 91 | w.entities.Delete(entity) 92 | entity.UpdateBounds() 93 | w.entities.Insert(entity) 94 | } 95 | 96 | func (w *World) RemovePlayer(p *Player) { 97 | w.entities.Delete(p) 98 | } 99 | 100 | func (w *World) ForEntitiesInRange(rect *math.Rect, cb func(entity entity.IEntity)) { 101 | for _, obj := range w.entities.SearchIntersect(rect) { 102 | cb(obj.(entity.IEntity)) 103 | } 104 | } 105 | 106 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 107 | // Server state sync 108 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 109 | 110 | func (w *World) syncBlockChanges() { 111 | // apply block changes 112 | for cpos, br := range OurWorld.BlockChanges { 113 | c := OurWorld.GetChunk(cpos.X, cpos.Z) 114 | 115 | for _, b := range br { 116 | c.SetBlockState(int(b.BlockX), int(b.BlockY), int(b.BlockZ), b.BlockState) 117 | world.QueueLightUpdate(w.World, c, int(b.BlockX), int(b.BlockY), int(b.BlockZ)) 118 | } 119 | } 120 | } 121 | 122 | func (w *World) syncState() { 123 | // sync the block changes 124 | w.syncBlockChanges() 125 | } 126 | 127 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 128 | // cleanup 129 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 130 | 131 | func (w *World) cleanup() { 132 | w.BlockChanges = make(map[world.ChunkPos][]play.BlockRecord) 133 | } 134 | 135 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 136 | // Others 137 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 138 | 139 | func (w *World) SendChunkToPlayer(x, z int, p *Player) { 140 | // load the chunk 141 | c := w.LoadChunk(x, z) 142 | 143 | // the chunk itself 144 | { 145 | writer := &minecraft.Writer{} 146 | c.MakeChunkDataPacket(writer) 147 | 148 | // TODO: block entities 149 | writer.WriteVarint(0) 150 | 151 | p.SendRaw(writer.Bytes()) 152 | } 153 | 154 | // send the light update 155 | { 156 | writer := &minecraft.Writer{} 157 | c.MakeUpdateLightPacket(writer) 158 | p.SendRaw(writer.Bytes()) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "C" 4 | import ( 5 | "flag" 6 | _ "github.com/itay2805/mcserver/config" 7 | "github.com/itay2805/mcserver/game" 8 | "github.com/itay2805/mcserver/server" 9 | "log" 10 | "os" 11 | "os/signal" 12 | "runtime" 13 | "runtime/pprof" 14 | "syscall" 15 | ) 16 | 17 | var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") 18 | var memprofile = flag.String("memprofile", "", "write memory profile to `file`") 19 | 20 | func main() { 21 | flag.Parse() 22 | 23 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 24 | // Profiling related code 25 | // 26 | var f *os.File 27 | if *cpuprofile != "" { 28 | f, err := os.Create(*cpuprofile) 29 | if err != nil { 30 | log.Fatal("could not create CPU profile: ", err) 31 | } 32 | defer f.Close() 33 | if err := pprof.StartCPUProfile(f); err != nil { 34 | log.Fatal("could not start CPU profile: ", err) 35 | } 36 | 37 | log.Println("CPU Profiler has started") 38 | } 39 | 40 | c := make(chan os.Signal, 2) 41 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 42 | go func() { 43 | for range c { 44 | if *memprofile != "" { 45 | f, err := os.Create(*memprofile) 46 | if err != nil { 47 | log.Fatal("could not create memory profile: ", err) 48 | } 49 | defer f.Close() 50 | runtime.GC() // get up-to-date statistics 51 | if err := pprof.WriteHeapProfile(f); err != nil { 52 | log.Fatal("could not write memory profile: ", err) 53 | } 54 | } 55 | 56 | pprof.StopCPUProfile() 57 | 58 | f.Close() 59 | os.Exit(0) 60 | } 61 | }() 62 | // 63 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 64 | 65 | log.Println("Everything is ready to start") 66 | 67 | // start the game loop 68 | go game.StartGameLoop() 69 | 70 | // start the server 71 | server.StartServer() 72 | } 73 | 74 | -------------------------------------------------------------------------------- /math/helpers.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | // Intersect computes the intersection of two rectangles. If no intersection 4 | // exists, the intersection is nil. 5 | func Intersect(r1, r2 *Rect) bool { 6 | // There are four cases of overlap: 7 | // 8 | // 1. a1------------b1 9 | // a2------------b2 10 | // p--------q 11 | // 12 | // 2. a1------------b1 13 | // a2------------b2 14 | // p--------q 15 | // 16 | // 3. a1-----------------b1 17 | // a2-------b2 18 | // p--------q 19 | // 20 | // 4. a1-------b1 21 | // a2-----------------b2 22 | // p--------q 23 | // 24 | // Thus there are only two cases of non-overlap: 25 | // 26 | // 1. a1------b1 27 | // a2------b2 28 | // 29 | // 2. a1------b1 30 | // a2------b2 31 | // 32 | // Enforced by constructor: a1 <= b1 and a2 <= b2. So we can just 33 | // check the endpoints. 34 | 35 | for i := range r1.p { 36 | a1, b1, a2, b2 := r1.p[i], r1.q[i], r2.p[i], r2.q[i] 37 | if b2 <= a1 || b1 <= a2 { 38 | return false 39 | } 40 | } 41 | return true 42 | } 43 | 44 | // BoundingBox constructs the smallest rectangle containing both r1 and r2. 45 | func BoundingBox(r1, r2 *Rect) (bb *Rect) { 46 | bb = new(Rect) 47 | dim := len(r1.p) 48 | bb.p = [3]float64{} 49 | bb.q = [3]float64{} 50 | for i := 0; i < dim; i++ { 51 | if r1.p[i] <= r2.p[i] { 52 | bb.p[i] = r1.p[i] 53 | } else { 54 | bb.p[i] = r2.p[i] 55 | } 56 | if r1.q[i] <= r2.q[i] { 57 | bb.q[i] = r2.q[i] 58 | } else { 59 | bb.q[i] = r1.q[i] 60 | } 61 | } 62 | return 63 | } 64 | 65 | // BoundingBoxN constructs the smallest rectangle containing all of r... 66 | func BoundingBoxN(rects ...*Rect) (bb *Rect) { 67 | if len(rects) == 1 { 68 | bb = rects[0] 69 | return 70 | } 71 | bb = BoundingBox(rects[0], rects[1]) 72 | for _, rect := range rects[2:] { 73 | bb = BoundingBox(bb, rect) 74 | } 75 | return 76 | } 77 | -------------------------------------------------------------------------------- /math/point.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import "math" 4 | 5 | // Point represents a point in n-dimensional Euclidean space. 6 | type Point [3]float64 7 | 8 | func NewPoint(x, y, z float64) Point { 9 | return Point{x, y, z} 10 | } 11 | 12 | func (p Point) X() float64 { 13 | return p[0] 14 | } 15 | 16 | func (p Point) Y() float64 { 17 | return p[1] 18 | } 19 | 20 | func (p Point) Z() float64 { 21 | return p[2] 22 | } 23 | 24 | func (p Point) SubScalar(num float64) Point { 25 | return Point{ p[0] - num, p[1] - num, p[2] - num } 26 | } 27 | 28 | // Dist computes the Euclidean distance between two points p and q. 29 | func (p Point) Dist(q Point) float64 { 30 | sum := 0.0 31 | for i := range p { 32 | dx := p[i] - q[i] 33 | sum += dx * dx 34 | } 35 | return math.Sqrt(sum) 36 | } 37 | 38 | // MinDist computes the square of the distance from a point to a rectangle. 39 | // If the point is contained in the rectangle then the distance is zero. 40 | // 41 | // Implemented per Definition 2 of "Nearest Neighbor Queries" by 42 | // N. Roussopoulos, S. Kelley and F. Vincent, ACM SIGMOD, pages 71-79, 1995. 43 | func (p Point) MinDist(r *Rect) float64 { 44 | sum := 0.0 45 | for i, pi := range p { 46 | if pi < r.p[i] { 47 | d := pi - r.p[i] 48 | sum += d * d 49 | } else if pi > r.q[i] { 50 | d := pi - r.q[i] 51 | sum += d * d 52 | } else { 53 | sum += 0 54 | } 55 | } 56 | return sum 57 | } 58 | 59 | 60 | // MinMaxDist computes the minimum of the maximum distances from p to points 61 | // on r. If r is the bounding box of some geometric objects, then there is 62 | // at least one object contained in r within MinMaxDist(p, r) of p. 63 | // 64 | // Implemented per Definition 4 of "Nearest Neighbor Queries" by 65 | // N. Roussopoulos, S. Kelley and F. Vincent, ACM SIGMOD, pages 71-79, 1995. 66 | func (p Point) MinMaxDist(r *Rect) float64 { 67 | // by definition, MinMaxDist(p, r) = 68 | // min{1<=k<=n}(|pk - rmk|^2 + sum{1<=i<=n, i != k}(|pi - rMi|^2)) 69 | // where rmk and rMk are defined as follows: 70 | 71 | rm := func(k int) float64 { 72 | if p[k] <= (r.p[k]+r.q[k])/2 { 73 | return r.p[k] 74 | } 75 | return r.q[k] 76 | } 77 | 78 | rM := func(k int) float64 { 79 | if p[k] >= (r.p[k]+r.q[k])/2 { 80 | return r.p[k] 81 | } 82 | return r.q[k] 83 | } 84 | 85 | // This formula can be computed in linear time by precomputing 86 | // S = sum{1<=i<=n}(|pi - rMi|^2). 87 | 88 | S := 0.0 89 | for i := range p { 90 | d := p[i] - rM(i) 91 | S += d * d 92 | } 93 | 94 | // Compute MinMaxDist using the precomputed S. 95 | min := math.MaxFloat64 96 | for k := range p { 97 | d1 := p[k] - rM(k) 98 | d2 := p[k] - rm(k) 99 | d := S - d1*d1 + d2*d2 100 | if d < min { 101 | min = d 102 | } 103 | } 104 | 105 | return min 106 | } 107 | 108 | // ToRect constructs a rectangle containing p with side lengths 2*tol. 109 | func (p Point) ToRect(tol float64) *Rect { 110 | a, b := [3]float64{}, [3]float64{} 111 | for i := range p { 112 | a[i] = p[i] - tol 113 | b[i] = p[i] + tol 114 | } 115 | return &Rect{a, b} 116 | } 117 | -------------------------------------------------------------------------------- /math/rect.go: -------------------------------------------------------------------------------- 1 | package math 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math" 7 | "strings" 8 | ) 9 | 10 | type DistError float64 11 | 12 | func (err DistError) Error() string { 13 | return "rtree: improper distance" 14 | } 15 | 16 | // Rect represents a subset of n-dimensional Euclidean space of the form 17 | // [a1, b1] x [a2, b2] x ... x [an, bn], where ai < bi for all 1 <= i <= n. 18 | type Rect struct { 19 | p, q Point // Enforced by NewRect: p[i] <= q[i] for all i. 20 | } 21 | 22 | // PointCoord returns the coordinate of the point of the rectangle at i 23 | func (r *Rect) PointCoord(i int) float64 { 24 | return r.p[i] 25 | } 26 | 27 | // LengthsCoord returns the coordinate of the lengths of the rectangle at i 28 | func (r *Rect) LengthsCoord(i int) float64 { 29 | return r.q[i] - r.p[i] 30 | } 31 | 32 | // Equal returns true if the two rectangles are equal 33 | func (r *Rect) Equal(other *Rect) bool { 34 | for i, e := range r.p { 35 | if e != other.p[i] { 36 | return false 37 | } 38 | } 39 | for i, e := range r.q { 40 | if e != other.q[i] { 41 | return false 42 | } 43 | } 44 | return true 45 | } 46 | 47 | func (r *Rect) String() string { 48 | s := make([]string, len(r.p)) 49 | for i, a := range r.p { 50 | b := r.q[i] 51 | s[i] = fmt.Sprintf("[%.2f, %.2f]", a, b) 52 | } 53 | return strings.Join(s, "x") 54 | } 55 | 56 | // NewRect constructs and returns a pointer to a Rect given a corner point and 57 | // the lengths of each dimension. The point p should be the most-negative point 58 | // on the rectangle (in every dimension) and every length should be positive. 59 | func NewRect(p Point, lengths [3]float64) (r *Rect) { 60 | r = new(Rect) 61 | r.p = p 62 | r.q = [3]float64{} 63 | for i := range p { 64 | if lengths[i] <= 0 { 65 | log.Panicln(DistError(lengths[i])) 66 | } 67 | r.q[i] = p[i] + lengths[i] 68 | } 69 | return 70 | } 71 | 72 | // NewRectFromPoints constructs and returns a pointer to a Rect given a corner points. 73 | func NewRectFromPoints(minPoint, maxPoint Point) *Rect { 74 | //checking that min and max points is swapping 75 | for i, p := range minPoint { 76 | if minPoint[i] > maxPoint[i] { 77 | minPoint[i] = maxPoint[i] 78 | maxPoint[i] = p 79 | } 80 | } 81 | 82 | return &Rect{p: minPoint, q: maxPoint} 83 | } 84 | 85 | // Size computes the measure of a rectangle (the product of its side lengths). 86 | func (r *Rect) Size() float64 { 87 | size := 1.0 88 | for i, a := range r.p { 89 | b := r.q[i] 90 | size *= b - a 91 | } 92 | return size 93 | } 94 | 95 | // Margin computes the sum of the edge lengths of a rectangle. 96 | func (r *Rect) Margin() float64 { 97 | // The number of edges in an n-dimensional rectangle is n * 2^(n-1) 98 | // (http://en.wikipedia.org/wiki/Hypercube_graph). Thus the number 99 | // of edges of length (ai - bi), where the rectangle is determined 100 | // by p = (a1, a2, ..., an) and q = (b1, b2, ..., bn), is 2^(n-1). 101 | // 102 | // The margin of the rectangle, then, is given by the formula 103 | // 2^(n-1) * [(b1 - a1) + (b2 - a2) + ... + (bn - an)]. 104 | dim := len(r.p) 105 | sum := 0.0 106 | for i, a := range r.p { 107 | b := r.q[i] 108 | sum += b - a 109 | } 110 | return math.Pow(2, float64(dim-1)) * sum 111 | } 112 | 113 | // ContainsPoint tests whether p is located inside or on the boundary of r. 114 | func (r *Rect) ContainsPoint(p Point) bool { 115 | for i, a := range p { 116 | // p is contained in (or on) r if and only if p <= a <= q for 117 | // every dimension. 118 | if a < r.p[i] || a > r.q[i] { 119 | return false 120 | } 121 | } 122 | 123 | return true 124 | } 125 | 126 | // ContainsRect tests whether r2 is is located inside r1. 127 | func (r *Rect) ContainsRect(r2 *Rect) bool { 128 | for i, a1 := range r.p { 129 | b1, a2, b2 := r.q[i], r2.p[i], r2.q[i] 130 | // enforced by constructor: a1 <= b1 and a2 <= b2. 131 | // so containment holds if and only if a1 <= a2 <= b2 <= b1 132 | // for every dimension. 133 | if a1 > a2 || b2 > b1 { 134 | return false 135 | } 136 | } 137 | 138 | return true 139 | } 140 | -------------------------------------------------------------------------------- /minecraft/biome/biomes.go: -------------------------------------------------------------------------------- 1 | package biome 2 | 3 | type Type int 4 | 5 | const ( 6 | TypeOcean = Type(iota) 7 | TypePlains 8 | TypeDesert 9 | TypeHills 10 | TypeForest 11 | TypeTaiga 12 | TypeSwamp 13 | TypeRiver 14 | TypeNether 15 | TypeSky 16 | TypeSnow 17 | TypeMushroomIsland 18 | TypeBeach 19 | TypeJungle 20 | TypeStoneBeach 21 | TypeSavanna 22 | TypeMesa 23 | TypeVoid 24 | ) 25 | 26 | type Biome struct { 27 | Id int 28 | Name string 29 | Type Type 30 | Rainfall float32 31 | Temperature float32 32 | } 33 | 34 | func AreSimilar(a, b *Biome) bool { 35 | if a == b { 36 | return true 37 | } 38 | 39 | if a == WoodedBadlandsPlateau || a == BadlandsPlateau { 40 | return b == WoodedBadlandsPlateau || b == BadlandsPlateau 41 | } 42 | 43 | return a.Type == b.Type 44 | } 45 | 46 | var shallowOceanBits = (1 << Ocean.Id) | 47 | (1 << FrozenOcean.Id) | 48 | (1 << WarmOcean.Id) | 49 | (1 << LukewarmOcean.Id) | 50 | (1 << ColdOcean.Id) 51 | 52 | func IsShallowOcean(b *Biome) bool { 53 | return ((1 << b.Id) & shallowOceanBits) != 0 54 | } 55 | 56 | var deepOceanBits = (1 << DeepOcean.Id) | 57 | (1 << DeepWarmOcean.Id) | 58 | (1 << DeepLukewarmOcean.Id) | 59 | (1 << DeepColdOcean.Id) | 60 | (1 << DeepFrozenOcean.Id) 61 | 62 | func IsDeepOcean(b *Biome) bool { 63 | return ((1 << b.Id) & deepOceanBits) != 0 64 | } 65 | 66 | 67 | var oceanBits = (1 << Ocean.Id) | 68 | (1 << FrozenOcean.Id) | 69 | (1 << WarmOcean.Id) | 70 | (1 << LukewarmOcean.Id) | 71 | (1 << ColdOcean.Id) | 72 | (1 << DeepOcean.Id) | 73 | (1 << DeepWarmOcean.Id) | 74 | (1 << DeepLukewarmOcean.Id) | 75 | (1 << DeepColdOcean.Id) | 76 | (1 << DeepFrozenOcean.Id) 77 | 78 | func IsOceanic(b *Biome) bool { 79 | return ((1 << b.Id) & oceanBits) != 0 80 | } 81 | -------------------------------------------------------------------------------- /minecraft/biome/data.go: -------------------------------------------------------------------------------- 1 | // Code generated by scripts/generate_biomes.go; DO NOT EDIT. 2 | // This file was generated by robots at 3 | // 2020-10-06 03:16:10.310621 4 | 5 | package biome 6 | 7 | var Ocean = &Biome{ 8 | Id: 0, 9 | Name: "ocean", 10 | Type: TypeOcean, 11 | Rainfall: 0.5, 12 | Temperature: 0.5, 13 | } 14 | var Plains = &Biome{ 15 | Id: 1, 16 | Name: "plains", 17 | Type: TypePlains, 18 | Rainfall: 0.4, 19 | Temperature: 0.8, 20 | } 21 | var Desert = &Biome{ 22 | Id: 2, 23 | Name: "desert", 24 | Type: TypeDesert, 25 | Rainfall: 0, 26 | Temperature: 2, 27 | } 28 | var Mountains = &Biome{ 29 | Id: 3, 30 | Name: "mountains", 31 | Type: TypeHills, 32 | Rainfall: 0.3, 33 | Temperature: 0.2, 34 | } 35 | var Forest = &Biome{ 36 | Id: 4, 37 | Name: "forest", 38 | Type: TypeForest, 39 | Rainfall: 0.8, 40 | Temperature: 0.7, 41 | } 42 | var Taiga = &Biome{ 43 | Id: 5, 44 | Name: "taiga", 45 | Type: TypeTaiga, 46 | Rainfall: 0.8, 47 | Temperature: 0.25, 48 | } 49 | var Swamp = &Biome{ 50 | Id: 6, 51 | Name: "swamp", 52 | Type: TypeSwamp, 53 | Rainfall: 0.9, 54 | Temperature: 0.8, 55 | } 56 | var River = &Biome{ 57 | Id: 7, 58 | Name: "river", 59 | Type: TypeRiver, 60 | Rainfall: 0.5, 61 | Temperature: 0.5, 62 | } 63 | var Nether = &Biome{ 64 | Id: 8, 65 | Name: "nether", 66 | Type: TypeNether, 67 | Rainfall: 0, 68 | Temperature: 2, 69 | } 70 | var TheEnd = &Biome{ 71 | Id: 9, 72 | Name: "the_end", 73 | Type: TypeSky, 74 | Rainfall: 0.5, 75 | Temperature: 0.5, 76 | } 77 | var FrozenOcean = &Biome{ 78 | Id: 10, 79 | Name: "frozen_ocean", 80 | Type: TypeOcean, 81 | Rainfall: 0.5, 82 | Temperature: 0, 83 | } 84 | var FrozenRiver = &Biome{ 85 | Id: 11, 86 | Name: "frozen_river", 87 | Type: TypeRiver, 88 | Rainfall: 0.5, 89 | Temperature: 0, 90 | } 91 | var SnowyTundra = &Biome{ 92 | Id: 12, 93 | Name: "snowy_tundra", 94 | Type: TypeSnow, 95 | Rainfall: 0.5, 96 | Temperature: 0, 97 | } 98 | var SnowyMountains = &Biome{ 99 | Id: 13, 100 | Name: "snowy_mountains", 101 | Type: TypeSnow, 102 | Rainfall: 0.5, 103 | Temperature: 0, 104 | } 105 | var MushroomFields = &Biome{ 106 | Id: 14, 107 | Name: "mushroom_fields", 108 | Type: TypeMushroomIsland, 109 | Rainfall: 1, 110 | Temperature: 0.9, 111 | } 112 | var MushroomFieldShore = &Biome{ 113 | Id: 15, 114 | Name: "mushroom_field_shore", 115 | Type: TypeMushroomIsland, 116 | Rainfall: 1, 117 | Temperature: 0.9, 118 | } 119 | var Beach = &Biome{ 120 | Id: 16, 121 | Name: "beach", 122 | Type: TypeBeach, 123 | Rainfall: 0.4, 124 | Temperature: 0.8, 125 | } 126 | var DesertHills = &Biome{ 127 | Id: 17, 128 | Name: "desert_hills", 129 | Type: TypeHills, 130 | Rainfall: 0, 131 | Temperature: 2, 132 | } 133 | var WoodedHills = &Biome{ 134 | Id: 18, 135 | Name: "wooded_hills", 136 | Type: TypeHills, 137 | Rainfall: 0.8, 138 | Temperature: 0.7, 139 | } 140 | var TaigaHills = &Biome{ 141 | Id: 19, 142 | Name: "taiga_hills", 143 | Type: TypeHills, 144 | Rainfall: 0.8, 145 | Temperature: 0.25, 146 | } 147 | var MountainEdge = &Biome{ 148 | Id: 20, 149 | Name: "mountain_edge", 150 | Type: TypeHills, 151 | Rainfall: 0.3, 152 | Temperature: 0.2, 153 | } 154 | var Jungle = &Biome{ 155 | Id: 21, 156 | Name: "jungle", 157 | Type: TypeJungle, 158 | Rainfall: 0.9, 159 | Temperature: 0.95, 160 | } 161 | var JungleHills = &Biome{ 162 | Id: 22, 163 | Name: "jungle_hills", 164 | Type: TypeHills, 165 | Rainfall: 0.9, 166 | Temperature: 0.95, 167 | } 168 | var JungleEdge = &Biome{ 169 | Id: 23, 170 | Name: "jungle_edge", 171 | Type: TypeJungle, 172 | Rainfall: 0.8, 173 | Temperature: 0.95, 174 | } 175 | var DeepOcean = &Biome{ 176 | Id: 24, 177 | Name: "deep_ocean", 178 | Type: TypeOcean, 179 | Rainfall: 0.5, 180 | Temperature: 0.5, 181 | } 182 | var StoneShore = &Biome{ 183 | Id: 25, 184 | Name: "stone_shore", 185 | Type: TypeStoneBeach, 186 | Rainfall: 0.3, 187 | Temperature: 0.2, 188 | } 189 | var SnowyBeach = &Biome{ 190 | Id: 26, 191 | Name: "snowy_beach", 192 | Type: TypeSnow, 193 | Rainfall: 0.3, 194 | Temperature: 0.05, 195 | } 196 | var BirchForest = &Biome{ 197 | Id: 27, 198 | Name: "birch_forest", 199 | Type: TypeForest, 200 | Rainfall: 0.6, 201 | Temperature: 0.6, 202 | } 203 | var BirchForestHills = &Biome{ 204 | Id: 28, 205 | Name: "birch_forest_hills", 206 | Type: TypeHills, 207 | Rainfall: 0.6, 208 | Temperature: 0.6, 209 | } 210 | var DarkForest = &Biome{ 211 | Id: 29, 212 | Name: "dark_forest", 213 | Type: TypeForest, 214 | Rainfall: 0.8, 215 | Temperature: 0.7, 216 | } 217 | var SnowyTaiga = &Biome{ 218 | Id: 30, 219 | Name: "snowy_taiga", 220 | Type: TypeTaiga, 221 | Rainfall: 0.4, 222 | Temperature: -0.5, 223 | } 224 | var SnowyTaigaHills = &Biome{ 225 | Id: 31, 226 | Name: "snowy_taiga_hills", 227 | Type: TypeHills, 228 | Rainfall: 0.4, 229 | Temperature: -0.5, 230 | } 231 | var GiantTreeTaiga = &Biome{ 232 | Id: 32, 233 | Name: "giant_tree_taiga", 234 | Type: TypeTaiga, 235 | Rainfall: 0.8, 236 | Temperature: 0.3, 237 | } 238 | var GiantTreeTaigaHills = &Biome{ 239 | Id: 33, 240 | Name: "giant_tree_taiga_hills", 241 | Type: TypeHills, 242 | Rainfall: 0.8, 243 | Temperature: 0.3, 244 | } 245 | var WoodedMountains = &Biome{ 246 | Id: 34, 247 | Name: "wooded_mountains", 248 | Type: TypeHills, 249 | Rainfall: 0.3, 250 | Temperature: 0.2, 251 | } 252 | var Savanna = &Biome{ 253 | Id: 35, 254 | Name: "savanna", 255 | Type: TypeSavanna, 256 | Rainfall: 0, 257 | Temperature: 1.2, 258 | } 259 | var SavannaPlateau = &Biome{ 260 | Id: 36, 261 | Name: "savanna_plateau", 262 | Type: TypeSavanna, 263 | Rainfall: 0, 264 | Temperature: 1, 265 | } 266 | var Badlands = &Biome{ 267 | Id: 37, 268 | Name: "badlands", 269 | Type: TypeMesa, 270 | Rainfall: 0, 271 | Temperature: 2, 272 | } 273 | var WoodedBadlandsPlateau = &Biome{ 274 | Id: 38, 275 | Name: "wooded_badlands_plateau", 276 | Type: TypeMesa, 277 | Rainfall: 0, 278 | Temperature: 2, 279 | } 280 | var BadlandsPlateau = &Biome{ 281 | Id: 39, 282 | Name: "badlands_plateau", 283 | Type: TypeMesa, 284 | Rainfall: 0, 285 | Temperature: 2, 286 | } 287 | var SmallEndIslands = &Biome{ 288 | Id: 40, 289 | Name: "small_end_islands", 290 | Type: TypeSky, 291 | Rainfall: 0.5, 292 | Temperature: 0.5, 293 | } 294 | var EndMidlands = &Biome{ 295 | Id: 41, 296 | Name: "end_midlands", 297 | Type: TypeSky, 298 | Rainfall: 0.5, 299 | Temperature: 0.5, 300 | } 301 | var EndHighlands = &Biome{ 302 | Id: 42, 303 | Name: "end_highlands", 304 | Type: TypeSky, 305 | Rainfall: 0.5, 306 | Temperature: 0.5, 307 | } 308 | var EndBarrens = &Biome{ 309 | Id: 43, 310 | Name: "end_barrens", 311 | Type: TypeSky, 312 | Rainfall: 0.5, 313 | Temperature: 0.5, 314 | } 315 | var WarmOcean = &Biome{ 316 | Id: 44, 317 | Name: "warm_ocean", 318 | Type: TypeOcean, 319 | Rainfall: 0.5, 320 | Temperature: 0.5, 321 | } 322 | var LukewarmOcean = &Biome{ 323 | Id: 45, 324 | Name: "lukewarm_ocean", 325 | Type: TypeOcean, 326 | Rainfall: 0.5, 327 | Temperature: 0.5, 328 | } 329 | var ColdOcean = &Biome{ 330 | Id: 46, 331 | Name: "cold_ocean", 332 | Type: TypeOcean, 333 | Rainfall: 0.5, 334 | Temperature: 0.5, 335 | } 336 | var DeepWarmOcean = &Biome{ 337 | Id: 47, 338 | Name: "deep_warm_ocean", 339 | Type: TypeOcean, 340 | Rainfall: 0.5, 341 | Temperature: 0.5, 342 | } 343 | var DeepLukewarmOcean = &Biome{ 344 | Id: 48, 345 | Name: "deep_lukewarm_ocean", 346 | Type: TypeOcean, 347 | Rainfall: 0.5, 348 | Temperature: 0.5, 349 | } 350 | var DeepColdOcean = &Biome{ 351 | Id: 49, 352 | Name: "deep_cold_ocean", 353 | Type: TypeOcean, 354 | Rainfall: 0.5, 355 | Temperature: 0.5, 356 | } 357 | var DeepFrozenOcean = &Biome{ 358 | Id: 50, 359 | Name: "deep_frozen_ocean", 360 | Type: TypeOcean, 361 | Rainfall: 0.5, 362 | Temperature: 0.5, 363 | } 364 | var TheVoid = &Biome{ 365 | Id: 127, 366 | Name: "the_void", 367 | Type: TypeVoid, 368 | Rainfall: 0.5, 369 | Temperature: 0.5, 370 | } 371 | var SunflowerPlains = &Biome{ 372 | Id: 129, 373 | Name: "sunflower_plains", 374 | Type: TypePlains, 375 | Rainfall: 0.4, 376 | Temperature: 0.8, 377 | } 378 | var DesertLakes = &Biome{ 379 | Id: 130, 380 | Name: "desert_lakes", 381 | Type: TypeDesert, 382 | Rainfall: 0, 383 | Temperature: 2, 384 | } 385 | var GravellyMountains = &Biome{ 386 | Id: 131, 387 | Name: "gravelly_mountains", 388 | Type: TypeHills, 389 | Rainfall: 0.3, 390 | Temperature: 0.2, 391 | } 392 | var FlowerForest = &Biome{ 393 | Id: 132, 394 | Name: "flower_forest", 395 | Type: TypeForest, 396 | Rainfall: 0.8, 397 | Temperature: 0.7, 398 | } 399 | var TaigaMountains = &Biome{ 400 | Id: 133, 401 | Name: "taiga_mountains", 402 | Type: TypeTaiga, 403 | Rainfall: 0.8, 404 | Temperature: 0.25, 405 | } 406 | var SwampHills = &Biome{ 407 | Id: 134, 408 | Name: "swamp_hills", 409 | Type: TypeHills, 410 | Rainfall: 0.9, 411 | Temperature: 0.8, 412 | } 413 | var IceSpikes = &Biome{ 414 | Id: 140, 415 | Name: "ice_spikes", 416 | Type: TypeSnow, 417 | Rainfall: 0.5, 418 | Temperature: 0, 419 | } 420 | var ModifiedJungle = &Biome{ 421 | Id: 149, 422 | Name: "modified_jungle", 423 | Type: TypeJungle, 424 | Rainfall: 0.9, 425 | Temperature: 0.95, 426 | } 427 | var ModifiedJungleEdge = &Biome{ 428 | Id: 151, 429 | Name: "modified_jungle_edge", 430 | Type: TypeJungle, 431 | Rainfall: 0.8, 432 | Temperature: 0.95, 433 | } 434 | var TallBirchForest = &Biome{ 435 | Id: 155, 436 | Name: "tall_birch_forest", 437 | Type: TypeForest, 438 | Rainfall: 0.6, 439 | Temperature: 0.6, 440 | } 441 | var TallBirchHills = &Biome{ 442 | Id: 156, 443 | Name: "tall_birch_hills", 444 | Type: TypeHills, 445 | Rainfall: 0.6, 446 | Temperature: 0.6, 447 | } 448 | var DarkForestHills = &Biome{ 449 | Id: 157, 450 | Name: "dark_forest_hills", 451 | Type: TypeHills, 452 | Rainfall: 0.8, 453 | Temperature: 0.7, 454 | } 455 | var SnowyTaigaMountains = &Biome{ 456 | Id: 158, 457 | Name: "snowy_taiga_mountains", 458 | Type: TypeTaiga, 459 | Rainfall: 0.4, 460 | Temperature: -0.5, 461 | } 462 | var GiantSpruceTaiga = &Biome{ 463 | Id: 160, 464 | Name: "giant_spruce_taiga", 465 | Type: TypeTaiga, 466 | Rainfall: 0.8, 467 | Temperature: 0.25, 468 | } 469 | var GiantSpruceTaigaHills = &Biome{ 470 | Id: 161, 471 | Name: "giant_spruce_taiga_hills", 472 | Type: TypeHills, 473 | Rainfall: 0.8, 474 | Temperature: 0.25, 475 | } 476 | var ModifiedGravellyMountains = &Biome{ 477 | Id: 162, 478 | Name: "modified_gravelly_mountains", 479 | Type: TypeHills, 480 | Rainfall: 0.3, 481 | Temperature: 0.2, 482 | } 483 | var ShatteredSavanna = &Biome{ 484 | Id: 163, 485 | Name: "shattered_savanna", 486 | Type: TypeSavanna, 487 | Rainfall: 0, 488 | Temperature: 1.1, 489 | } 490 | var ShatteredSavannaPlateau = &Biome{ 491 | Id: 164, 492 | Name: "shattered_savanna_plateau", 493 | Type: TypeSavanna, 494 | Rainfall: 0, 495 | Temperature: 1, 496 | } 497 | var ErodedBadlands = &Biome{ 498 | Id: 165, 499 | Name: "eroded_badlands", 500 | Type: TypeMesa, 501 | Rainfall: 0, 502 | Temperature: 2, 503 | } 504 | var ModifiedWoodedBadlandsPlateau = &Biome{ 505 | Id: 166, 506 | Name: "modified_wooded_badlands_plateau", 507 | Type: TypeMesa, 508 | Rainfall: 0, 509 | Temperature: 2, 510 | } 511 | var ModifiedBadlandsPlateau = &Biome{ 512 | Id: 167, 513 | Name: "modified_badlands_plateau", 514 | Type: TypeMesa, 515 | Rainfall: 0, 516 | Temperature: 2, 517 | } 518 | var BambooJungle = &Biome{ 519 | Id: 168, 520 | Name: "bamboo_jungle", 521 | Type: TypeJungle, 522 | Rainfall: 0.9, 523 | Temperature: 0.95, 524 | } 525 | var BambooJungleHills = &Biome{ 526 | Id: 169, 527 | Name: "bamboo_jungle_hills", 528 | Type: TypeHills, 529 | Rainfall: 0.9, 530 | Temperature: 0.95, 531 | } 532 | 533 | var biomes = [...]*Biome{ 534 | 0: Ocean, 535 | 1: Plains, 536 | 2: Desert, 537 | 3: Mountains, 538 | 4: Forest, 539 | 5: Taiga, 540 | 6: Swamp, 541 | 7: River, 542 | 8: Nether, 543 | 9: TheEnd, 544 | 10: FrozenOcean, 545 | 11: FrozenRiver, 546 | 12: SnowyTundra, 547 | 13: SnowyMountains, 548 | 14: MushroomFields, 549 | 15: MushroomFieldShore, 550 | 16: Beach, 551 | 17: DesertHills, 552 | 18: WoodedHills, 553 | 19: TaigaHills, 554 | 20: MountainEdge, 555 | 21: Jungle, 556 | 22: JungleHills, 557 | 23: JungleEdge, 558 | 24: DeepOcean, 559 | 25: StoneShore, 560 | 26: SnowyBeach, 561 | 27: BirchForest, 562 | 28: BirchForestHills, 563 | 29: DarkForest, 564 | 30: SnowyTaiga, 565 | 31: SnowyTaigaHills, 566 | 32: GiantTreeTaiga, 567 | 33: GiantTreeTaigaHills, 568 | 34: WoodedMountains, 569 | 35: Savanna, 570 | 36: SavannaPlateau, 571 | 37: Badlands, 572 | 38: WoodedBadlandsPlateau, 573 | 39: BadlandsPlateau, 574 | 40: SmallEndIslands, 575 | 41: EndMidlands, 576 | 42: EndHighlands, 577 | 43: EndBarrens, 578 | 44: WarmOcean, 579 | 45: LukewarmOcean, 580 | 46: ColdOcean, 581 | 47: DeepWarmOcean, 582 | 48: DeepLukewarmOcean, 583 | 49: DeepColdOcean, 584 | 50: DeepFrozenOcean, 585 | 127: TheVoid, 586 | 129: SunflowerPlains, 587 | 130: DesertLakes, 588 | 131: GravellyMountains, 589 | 132: FlowerForest, 590 | 133: TaigaMountains, 591 | 134: SwampHills, 592 | 140: IceSpikes, 593 | 149: ModifiedJungle, 594 | 151: ModifiedJungleEdge, 595 | 155: TallBirchForest, 596 | 156: TallBirchHills, 597 | 157: DarkForestHills, 598 | 158: SnowyTaigaMountains, 599 | 160: GiantSpruceTaiga, 600 | 161: GiantSpruceTaigaHills, 601 | 162: ModifiedGravellyMountains, 602 | 163: ShatteredSavanna, 603 | 164: ShatteredSavannaPlateau, 604 | 165: ErodedBadlands, 605 | 166: ModifiedWoodedBadlandsPlateau, 606 | 167: ModifiedBadlandsPlateau, 607 | 168: BambooJungle, 608 | 169: BambooJungleHills, 609 | } 610 | 611 | -------------------------------------------------------------------------------- /minecraft/block/block.go: -------------------------------------------------------------------------------- 1 | package block 2 | 3 | import ( 4 | "fmt" 5 | "github.com/itay2805/mcserver/minecraft/item" 6 | ) 7 | 8 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 9 | // Base block type 10 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | // the base block type 13 | type Block struct { 14 | // The identification of this block 15 | Id int 16 | Name string 17 | 18 | // The item corresponding to this block 19 | // if any 20 | Item *item.Item 21 | 22 | // state id 23 | MinStateId uint16 24 | MaxStateId uint16 25 | DefaultStateId uint16 26 | 27 | // is this a solid block 28 | Solid bool 29 | 30 | // light related values 31 | Transparent bool 32 | FilterLight int 33 | EmitLight int 34 | } 35 | 36 | func (b *Block) String() string { 37 | return fmt.Sprintf("Block{ Name: \"%s\" }", b.Name) 38 | } 39 | 40 | func FromItem(item *item.Item) *Block { 41 | if item.ID > len(blockByItemId) { 42 | return nil 43 | } 44 | return blockByItemId[item.ID] 45 | } 46 | 47 | func GetById(stateId int) *Block { 48 | return blocks[stateId] 49 | } 50 | 51 | func GetByStateId(stateId uint16) *Block { 52 | return stateIdToBlockId[stateId] 53 | } 54 | 55 | func boolToMeta(val bool) uint16 { 56 | if val { 57 | return 0 58 | } else { 59 | return 1 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /minecraft/block/meta.go: -------------------------------------------------------------------------------- 1 | // Code generated by scripts/generate_meta.go; DO NOT EDIT. 2 | // This file was generated by robots at 3 | // 2020-10-09 18:54:01.880886 4 | 5 | package block 6 | 7 | import "github.com/itay2805/mcserver/minecraft" 8 | 9 | type StairsMeta struct { 10 | Facing minecraft.Face 11 | Half minecraft.Face 12 | Shape minecraft.Shape 13 | Waterlogged bool 14 | } 15 | 16 | func (m *StairsMeta) FromMeta(meta uint16) { 17 | m.Waterlogged = (meta % 2) == 1 18 | meta /= 2 19 | m.Shape = minecraft.Shape(meta % 5) 20 | meta /= 5 21 | m.Half = minecraft.Face(meta % 2) 22 | meta /= 2 23 | m.Facing = minecraft.Face((meta % 4) + 2) 24 | meta /= 4 25 | } 26 | 27 | func (m *StairsMeta) ToMeta() uint16 { 28 | meta := uint16(0) 29 | meta *= 4 30 | meta += uint16(m.Facing) - 2 31 | meta *= 2 32 | meta += uint16(m.Half) 33 | meta *= 5 34 | meta += uint16(m.Shape) 35 | meta *= 2 36 | meta += boolToMeta(m.Waterlogged) 37 | return meta 38 | } 39 | 40 | type FurnaceMeta struct { 41 | Facing minecraft.Face 42 | Lit bool 43 | } 44 | 45 | func (m *FurnaceMeta) FromMeta(meta uint16) { 46 | m.Lit = (meta % 2) == 1 47 | meta /= 2 48 | m.Facing = minecraft.Face((meta % 4) + 2) 49 | meta /= 4 50 | } 51 | 52 | func (m *FurnaceMeta) ToMeta() uint16 { 53 | meta := uint16(0) 54 | meta *= 4 55 | meta += uint16(m.Facing) - 2 56 | meta *= 2 57 | meta += boolToMeta(m.Lit) 58 | return meta 59 | } 60 | 61 | -------------------------------------------------------------------------------- /minecraft/chat.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | type Chat struct { 9 | Text string `json:"text,omitempty"` 10 | Bold bool `json:"bold,omitempty"` 11 | Italic bool `json:"italic,omitempty"` 12 | Underlined bool `json:"underlined,omitempty"` 13 | Strikethrough bool `json:"strikethrough,omitempty"` 14 | Obfuscated bool `json:"obfuscated,omitempty"` 15 | Color string `json:"color,omitempty"` 16 | Translate string `json:"translate,omitempty"` 17 | With []Chat `json:"with,omitempty"` 18 | Extra []Chat `json:"extra,omitempty"` 19 | } 20 | 21 | func NewChat(js []byte) Chat { 22 | m := Chat{} 23 | if js[0] == '"' { 24 | err := json.Unmarshal(js, &m.Text) 25 | if err != nil { 26 | log.Panicln("Error decoding chat object") 27 | } 28 | } else { 29 | err := json.Unmarshal(js, m) 30 | if err != nil { 31 | log.Panicln("Error decoding chat object") 32 | } 33 | } 34 | return m 35 | } 36 | 37 | func (m Chat) ToJSON() []byte { 38 | code, err := json.Marshal(m) 39 | if err != nil { 40 | log.Panicln(err) 41 | } 42 | return code 43 | } 44 | 45 | func Text(str string) Chat { 46 | return Chat{ 47 | Text: str, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /minecraft/chunk/chunk.go: -------------------------------------------------------------------------------- 1 | package chunk 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/common" 5 | "github.com/itay2805/mcserver/minecraft" 6 | ) 7 | 8 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 9 | // Helper functions to get the index to certain arrays in the chunk 10 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | func getSectionIndex(y int) int { 13 | return y >> 4 14 | } 15 | 16 | func getLightSection(y int) int { 17 | return (y >> 4) + 1 18 | } 19 | 20 | func getBiomeIndex(x, y, z int) int { 21 | return ((y >> 2) & 63) << 4 | ((z >> 2) & 3) << 2 | ((x >> 2) & 3) 22 | } 23 | 24 | func getSectionBlockIndex(x, y, z int) int { 25 | return ((y & 0xf) << 8) | (z << 4) | x 26 | } 27 | 28 | func getHeightMapIndex(x, z int) int { 29 | return x * 16 + z 30 | } 31 | 32 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 33 | // The chunk itself 34 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 35 | 36 | type Chunk struct { 37 | X, Z int 38 | 39 | // block related data 40 | sections [NumSections]*section 41 | 42 | // biome related data 43 | biomes [4 * 4 * 64]int32 44 | 45 | // light related data 46 | skyLightSections [NumLightSections]*[LightVolume]uint8 47 | blockLightSections [NumLightSections]*[LightVolume]uint8 48 | } 49 | 50 | func NewChunk(x, z int) *Chunk { 51 | return &Chunk{ 52 | X: x, 53 | Z: z, 54 | } 55 | } 56 | 57 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 58 | // Helpers to get values from the chunk 59 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 60 | 61 | func (c *Chunk) GetBlockState(x, y, z int) uint16 { 62 | sec := c.sections[getSectionIndex(y)] 63 | if sec == nil { 64 | return 0 65 | } 66 | 67 | return sec.ids[getSectionBlockIndex(x, y, z)] 68 | } 69 | 70 | func (c *Chunk) SetBlockState(x, y, z int, state uint16) { 71 | sec := c.sections[getSectionIndex(y)] 72 | if sec == nil { 73 | // don't insert a new section if this is an air block 74 | if state == 0 { 75 | return 76 | } 77 | sec = §ion{} 78 | c.sections[getSectionIndex(y)] = sec 79 | } 80 | 81 | // get the old state for updating the block count 82 | // NOTE: This assumes air is always 0 83 | oldState := sec.ids[getSectionBlockIndex(x, y, z)] 84 | 85 | // set the block count 86 | if oldState != 0 { 87 | if state == 0 { 88 | // was a block, not anymore 89 | sec.blockCount-- 90 | } 91 | } else if state != 0 { 92 | // was air, not anymore 93 | sec.blockCount++ 94 | } 95 | 96 | // set the block as needed 97 | if sec.blockCount == 0 { 98 | // no more blocks, remove section 99 | c.sections[getSectionIndex(y)] = nil 100 | } else { 101 | // set the block 102 | sec.ids[getSectionBlockIndex(x, y, z)] = state 103 | sec.palette = nil 104 | } 105 | } 106 | 107 | func (c *Chunk) GetSkyLight(x, y, z int) int { 108 | sec := c.skyLightSections[getLightSection(y)] 109 | if sec == nil { 110 | return 0 111 | } 112 | 113 | idx := getSectionBlockIndex(x, y, z) 114 | half := idx >> 1 115 | 116 | if (idx & 1) == 1 { 117 | return int(sec[half] >> 4) 118 | } else { 119 | return int(sec[half] & 0xf) 120 | } 121 | } 122 | 123 | func (c *Chunk) SetSkyLight(x, y, z int, light int) { 124 | sec := c.skyLightSections[getLightSection(y)] 125 | if sec == nil { 126 | if light == 0 { 127 | // ignore if setting light to 0 128 | return 129 | } 130 | sec = &[LightVolume]uint8{} 131 | c.skyLightSections[getLightSection(y)] = sec 132 | } 133 | 134 | val := uint8(light & 0xf) 135 | idx := getSectionBlockIndex(x, y, z) 136 | half := idx >> 1 137 | 138 | if (idx & 1) == 1 { 139 | sec[half] = (sec[half] & 0x0f) | (val << 4) 140 | } else { 141 | sec[half] = (sec[half] & 0xf0) | val 142 | } 143 | } 144 | 145 | func (c *Chunk) GetBlockLight(x, y, z int) int { 146 | sec := c.blockLightSections[getLightSection(y)] 147 | if sec == nil { 148 | return 0 149 | } 150 | 151 | idx := getSectionBlockIndex(x, y, z) 152 | half := idx >> 1 153 | 154 | if (idx & 1) == 1 { 155 | return int(sec[half] >> 4) 156 | } else { 157 | return int(sec[half] & 0xf) 158 | } 159 | } 160 | 161 | func (c *Chunk) SetBlockLight(x, y, z int, light int) { 162 | sec := c.blockLightSections[getLightSection(y)] 163 | if sec == nil { 164 | if light == 0 { 165 | // ignore if setting light to 0 166 | return 167 | } 168 | sec = &[LightVolume]uint8{} 169 | c.blockLightSections[getLightSection(y)] = sec 170 | } 171 | 172 | val := uint8(light & 0xf) 173 | idx := getSectionBlockIndex(x, y, z) 174 | half := idx >> 1 175 | 176 | if (idx & 1) == 1 { 177 | sec[half] = (sec[half] & 0x0f) | (val << 4) 178 | } else { 179 | sec[half] = (sec[half] & 0xf0) | val 180 | } 181 | } 182 | 183 | // 184 | // Write the chunk data into the given writer in the Chunk Data format 185 | // 186 | // NOTE: This does not include block entities! these are managed by the world 187 | // so they need to be written by the world as well! 188 | // 189 | func (c *Chunk) MakeChunkDataPacket(writer *minecraft.Writer) { 190 | // prepare the data 191 | primaryBitMask := int32(0) 192 | dataSize := 0 193 | for i, sec := range c.sections { 194 | if sec == nil { 195 | continue 196 | } 197 | 198 | // we have data 199 | primaryBitMask |= 1 << i 200 | 201 | // generate the palette 202 | // no need to save because this is cached 203 | palette := sec.generatePalette() 204 | 205 | dataSize += 2 // block count 206 | dataSize += 1 // bits per block 207 | 208 | // if this is less or equals to 8 then 209 | // account for the palette 210 | if palette.bitsPerBlock <= MaxBitsPerBlock { 211 | // the size of the number of blocks in the palette 212 | dataSize += common.VarintSize(int32(len(palette.ids))) 213 | 214 | // the size of each if in the palette 215 | for _, id := range palette.ids { 216 | dataSize += common.VarintSize(int32(id)) 217 | } 218 | } 219 | 220 | // data array length + data sizes 221 | arrayLen := common.CompactArrayLength(int(palette.bitsPerBlock), 4096) 222 | dataSize += common.VarintSize(int32(arrayLen)) 223 | dataSize += arrayLen * 8 224 | } 225 | 226 | // grow to fit the whole packet 227 | writer.Grow(common.VarintSize(0x22) + 228 | 4 + 4 + 1 + 4096 + 229 | common.VarintSize(primaryBitMask) + 230 | common.VarintSize(int32(dataSize)) + 231 | dataSize) 232 | 233 | // write the header 234 | writer.WriteVarint(0x22) 235 | writer.WriteInt(int32(c.X)) 236 | writer.WriteInt(int32(c.Z)) 237 | writer.WriteBoolean(true) 238 | 239 | // write the headers 240 | writer.WriteVarint(primaryBitMask) 241 | 242 | // we don't send height maps to the client 243 | nbt := writer.StartNBT() 244 | nbt.StartCompound("") 245 | nbt.EndCompound() 246 | 247 | // Write the biomes 248 | for _, biome := range c.biomes { 249 | writer.WriteInt(biome) 250 | } 251 | 252 | // write the chunk data 253 | writer.WriteVarint(int32(dataSize)) 254 | for _, sec := range c.sections { 255 | if sec == nil { 256 | continue 257 | } 258 | 259 | // generate the palette 260 | // no need to save because this is cached 261 | palette := sec.generatePalette() 262 | 263 | writer.WriteShort(int16(sec.blockCount)) 264 | writer.WriteByte(palette.bitsPerBlock) 265 | 266 | // if this is less or equals to 8 then 267 | // account for the palette 268 | direct := palette.bitsPerBlock > MaxBitsPerBlock 269 | if !direct { 270 | // the size of the number of blocks in the palette 271 | writer.WriteVarint(int32(len(palette.ids))) 272 | 273 | // the size of each if in the palette 274 | for _, id := range palette.ids { 275 | writer.WriteVarint(int32(id)) 276 | } 277 | } 278 | 279 | // prepare the array 280 | packed := common.NewCompactArray(int(palette.bitsPerBlock), 4096) 281 | if direct { 282 | // for direct just pack all the items 283 | for i, id := range sec.ids { 284 | packed.Set(i, int(id)) 285 | } 286 | } else { 287 | // for indirect use the map to figure 288 | // the id to put 289 | for i, id := range sec.ids { 290 | packed.Set(i, palette.indexMap[id]) 291 | } 292 | } 293 | 294 | // data array length + data 295 | writer.WriteVarint(int32(len(packed.Values))) 296 | for _, l := range packed.Values { 297 | writer.WriteLong(l) 298 | } 299 | } 300 | } 301 | 302 | // TODO: maybe cache this 303 | func (c *Chunk) MakeUpdateLightPacket(writer *minecraft.Writer) { 304 | // prepare the masks 305 | // NOTE: This assumes the amount of sky light sections is 306 | // the same as block light sections, might need to be 307 | // changed in the future 308 | skyLightMask := int32(0) 309 | blockLightMask := int32(0) 310 | count := 0 311 | for i := 0; i < NumLightSections; i++ { 312 | if c.skyLightSections[i] != nil { 313 | skyLightMask |= 1 << i 314 | count++ 315 | } 316 | 317 | if c.blockLightSections[i] != nil { 318 | blockLightMask |= 1 << i 319 | count++ 320 | } 321 | } 322 | 323 | // we already know everything so just grow it properly 324 | writer.Grow( 325 | common.VarintSize(0x25) + 326 | common.VarintSize(int32(c.X)) + 327 | common.VarintSize(int32(c.Z)) + 328 | common.VarintSize(skyLightMask) + common.VarintSize(blockLightMask) + 329 | common.VarintSize(0) + common.VarintSize(0) + 330 | (common.VarintSize(2048) + 2048) * 18 * 2) 331 | 332 | // write the basic data 333 | writer.WriteVarint(0x25) 334 | writer.WriteVarint(int32(c.X)) 335 | writer.WriteVarint(int32(c.Z)) 336 | 337 | // write the masks 338 | writer.WriteVarint(skyLightMask) 339 | writer.WriteVarint(blockLightMask) 340 | writer.WriteVarint(0) 341 | writer.WriteVarint(0) 342 | 343 | // write out all of the sky light arrays 344 | for _, sec := range c.skyLightSections { 345 | if sec == nil { 346 | continue 347 | } 348 | 349 | writer.WriteVarint(int32(len(sec))) 350 | writer.WriteBytes(sec[:]) 351 | } 352 | 353 | // write out all of the block light arrays 354 | for _, sec := range c.blockLightSections { 355 | if sec == nil { 356 | continue 357 | } 358 | 359 | writer.WriteVarint(int32(len(sec))) 360 | writer.WriteBytes(sec[:]) 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /minecraft/chunk/constants.go: -------------------------------------------------------------------------------- 1 | package chunk 2 | 3 | const ( 4 | // height in blocks of a chunk column 5 | ChunkHeight = 256 6 | 7 | // width in blocks of a chunk column 8 | ChunkWidth = 16 9 | 10 | // height in blocks of a chunk section 11 | SectionHeight = 16 12 | 13 | // width in blocks of a chunk section 14 | SectionWidth = 16 15 | 16 | // volume in blocks of a chunk section 17 | SectionVolume = SectionHeight * SectionWidth * SectionWidth 18 | 19 | // number of chunk sections in a chunk column 20 | NumSections = 16 21 | 22 | // number of light sections in a chunk column 23 | NumLightSections = NumSections + 2 24 | 25 | // number of elements in the light byte array 26 | LightVolume = (SectionHeight * SectionWidth * SectionWidth) / 2 27 | 28 | // maximum number of bits per block allowed when using the section palette. 29 | // values above will switch to global palette 30 | MaxBitsPerBlock = 8 31 | 32 | // number of bits used for each block in the global palette. 33 | // this value should not be hardcoded according to wiki.vg 34 | GlobalBitsPerBlock = 14 35 | ) 36 | -------------------------------------------------------------------------------- /minecraft/chunk/palette.go: -------------------------------------------------------------------------------- 1 | package chunk 2 | 3 | import "math/bits" 4 | 5 | // NOTE: The index map has enough elements based on the highest block state 6 | // whenever updating the version of minecraft you need to modify this 7 | // number as well 8 | type palette struct { 9 | ids []uint16 10 | indexMap [11337]int 11 | bitsPerBlock byte 12 | } 13 | 14 | func (p *palette) computeBitsPerBlock() { 15 | num := uint(len(p.ids)) 16 | if num == 0 { 17 | p.bitsPerBlock = 4 18 | } else { 19 | res := bits.Len(num) 20 | if (num & (num - 1)) != 0 { 21 | res++ 22 | } 23 | 24 | if res < 4 { 25 | res = 4 26 | } 27 | 28 | p.bitsPerBlock = byte(res) 29 | } 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /minecraft/chunk/section.go: -------------------------------------------------------------------------------- 1 | package chunk 2 | 3 | type section struct { 4 | blockCount int 5 | ids [4096]uint16 6 | palette *palette 7 | } 8 | 9 | func (c *section) generatePalette() *palette { 10 | if c.palette != nil { 11 | return c.palette 12 | } 13 | 14 | c.palette = &palette{} 15 | 16 | // set everything to unused 17 | for i := range c.palette.indexMap { 18 | c.palette.indexMap[i] = -1 19 | } 20 | 21 | // populate it 22 | for _, id := range c.ids { 23 | if c.palette.indexMap[id] == -1 { 24 | c.palette.indexMap[id] = len(c.palette.ids) 25 | c.palette.ids = append(c.palette.ids, id) 26 | } 27 | } 28 | 29 | c.palette.computeBitsPerBlock() 30 | 31 | return c.palette 32 | } 33 | -------------------------------------------------------------------------------- /minecraft/entity/data.go: -------------------------------------------------------------------------------- 1 | // Code generated by scripts/generate_entities.go; DO NOT EDIT. 2 | // This file was generated by robots at 3 | // 2020-10-05 22:34:01.651706 4 | 5 | package entity 6 | 7 | var types = [...]Type{ 8 | 0: { 9 | Id: 0, 10 | Name: "area_effect_cloud", 11 | Width: 6, 12 | Height: 0.5, 13 | }, 14 | 1: { 15 | Id: 1, 16 | Name: "armor_stand", 17 | Width: 0.5, 18 | Height: 1.975, 19 | }, 20 | 2: { 21 | Id: 2, 22 | Name: "arrow", 23 | Width: 0.5, 24 | Height: 0.5, 25 | }, 26 | 3: { 27 | Id: 3, 28 | Name: "bat", 29 | Width: 0.5, 30 | Height: 0.9, 31 | }, 32 | 4: { 33 | Id: 4, 34 | Name: "bee", 35 | Width: 0.7, 36 | Height: 0.6, 37 | }, 38 | 5: { 39 | Id: 5, 40 | Name: "blaze", 41 | Width: 0.6, 42 | Height: 1.8, 43 | }, 44 | 6: { 45 | Id: 6, 46 | Name: "boat", 47 | Width: 1.375, 48 | Height: 0.5625, 49 | }, 50 | 7: { 51 | Id: 7, 52 | Name: "cat", 53 | Width: 0.6, 54 | Height: 0.7, 55 | }, 56 | 8: { 57 | Id: 8, 58 | Name: "cave_spider", 59 | Width: 0.7, 60 | Height: 0.5, 61 | }, 62 | 9: { 63 | Id: 9, 64 | Name: "chicken", 65 | Width: 0.4, 66 | Height: 0.7, 67 | }, 68 | 10: { 69 | Id: 10, 70 | Name: "cod", 71 | Width: 0.5, 72 | Height: 0.3, 73 | }, 74 | 11: { 75 | Id: 11, 76 | Name: "cow", 77 | Width: 0.9, 78 | Height: 1.4, 79 | }, 80 | 12: { 81 | Id: 12, 82 | Name: "creeper", 83 | Width: 0.6, 84 | Height: 1.7, 85 | }, 86 | 13: { 87 | Id: 13, 88 | Name: "donkey", 89 | Width: 1.39648, 90 | Height: 1.5, 91 | }, 92 | 14: { 93 | Id: 14, 94 | Name: "dolphin", 95 | Width: 0.9, 96 | Height: 0.6, 97 | }, 98 | 15: { 99 | Id: 15, 100 | Name: "dragon_fireball", 101 | Width: 1, 102 | Height: 1, 103 | }, 104 | 16: { 105 | Id: 16, 106 | Name: "drowned", 107 | Width: 0.6, 108 | Height: 1.95, 109 | }, 110 | 17: { 111 | Id: 17, 112 | Name: "elder_guardian", 113 | Width: 1.9975, 114 | Height: 1.9975, 115 | }, 116 | 18: { 117 | Id: 18, 118 | Name: "end_crystal", 119 | Width: 2, 120 | Height: 2, 121 | }, 122 | 19: { 123 | Id: 19, 124 | Name: "ender_dragon", 125 | Width: 16, 126 | Height: 8, 127 | }, 128 | 20: { 129 | Id: 20, 130 | Name: "enderman", 131 | Width: 0.6, 132 | Height: 2.9, 133 | }, 134 | 21: { 135 | Id: 21, 136 | Name: "endermite", 137 | Width: 0.4, 138 | Height: 0.3, 139 | }, 140 | 22: { 141 | Id: 22, 142 | Name: "evoker_fangs", 143 | Width: 0.5, 144 | Height: 0.8, 145 | }, 146 | 23: { 147 | Id: 23, 148 | Name: "evoker", 149 | Width: 0.6, 150 | Height: 1.95, 151 | }, 152 | 24: { 153 | Id: 24, 154 | Name: "experience_orb", 155 | Width: 0.5, 156 | Height: 0.5, 157 | }, 158 | 25: { 159 | Id: 25, 160 | Name: "eye_of_ender", 161 | Width: 0.25, 162 | Height: 0.25, 163 | }, 164 | 26: { 165 | Id: 26, 166 | Name: "falling_block", 167 | Width: 0.98, 168 | Height: 0.98, 169 | }, 170 | 27: { 171 | Id: 27, 172 | Name: "firework_rocket", 173 | Width: 0.25, 174 | Height: 0.25, 175 | }, 176 | 28: { 177 | Id: 28, 178 | Name: "fox", 179 | Width: 0.6, 180 | Height: 0.7, 181 | }, 182 | 29: { 183 | Id: 29, 184 | Name: "ghast", 185 | Width: 4, 186 | Height: 4, 187 | }, 188 | 30: { 189 | Id: 30, 190 | Name: "giant", 191 | Width: 3.6, 192 | Height: 12, 193 | }, 194 | 31: { 195 | Id: 31, 196 | Name: "guardian", 197 | Width: 0.85, 198 | Height: 0.85, 199 | }, 200 | 32: { 201 | Id: 32, 202 | Name: "horse", 203 | Width: 1.39648, 204 | Height: 1.6, 205 | }, 206 | 33: { 207 | Id: 33, 208 | Name: "husk", 209 | Width: 0.6, 210 | Height: 1.95, 211 | }, 212 | 34: { 213 | Id: 34, 214 | Name: "illusioner", 215 | Width: 0.6, 216 | Height: 1.95, 217 | }, 218 | 35: { 219 | Id: 35, 220 | Name: "item", 221 | Width: 0.25, 222 | Height: 0.25, 223 | }, 224 | 36: { 225 | Id: 36, 226 | Name: "item_frame", 227 | Width: 0.5, 228 | Height: 0.5, 229 | }, 230 | 37: { 231 | Id: 37, 232 | Name: "fireball", 233 | Width: 1, 234 | Height: 1, 235 | }, 236 | 38: { 237 | Id: 38, 238 | Name: "leash_knot", 239 | Width: 0.5, 240 | Height: 0.5, 241 | }, 242 | 39: { 243 | Id: 39, 244 | Name: "llama", 245 | Width: 0.9, 246 | Height: 1.87, 247 | }, 248 | 40: { 249 | Id: 40, 250 | Name: "llama_spit", 251 | Width: 0.25, 252 | Height: 0.25, 253 | }, 254 | 41: { 255 | Id: 41, 256 | Name: "magma_cube", 257 | Width: 2.04, 258 | Height: 2.04, 259 | }, 260 | 42: { 261 | Id: 42, 262 | Name: "minecart", 263 | Width: 0.98, 264 | Height: 0.7, 265 | }, 266 | 43: { 267 | Id: 43, 268 | Name: "chest_minecart", 269 | Width: 0.98, 270 | Height: 0.7, 271 | }, 272 | 44: { 273 | Id: 44, 274 | Name: "command_block_minecart", 275 | Width: 0.98, 276 | Height: 0.7, 277 | }, 278 | 45: { 279 | Id: 45, 280 | Name: "furnace_minecart", 281 | Width: 0.98, 282 | Height: 0.7, 283 | }, 284 | 46: { 285 | Id: 46, 286 | Name: "hopper_minecart", 287 | Width: 0.98, 288 | Height: 0.7, 289 | }, 290 | 47: { 291 | Id: 47, 292 | Name: "spawner_minecart", 293 | Width: 0.98, 294 | Height: 0.7, 295 | }, 296 | 48: { 297 | Id: 48, 298 | Name: "tnt_minecart", 299 | Width: 0.98, 300 | Height: 0.7, 301 | }, 302 | 49: { 303 | Id: 49, 304 | Name: "mule", 305 | Width: 1.39648, 306 | Height: 1.6, 307 | }, 308 | 50: { 309 | Id: 50, 310 | Name: "mooshroom", 311 | Width: 0.9, 312 | Height: 1.4, 313 | }, 314 | 51: { 315 | Id: 51, 316 | Name: "ocelot", 317 | Width: 0.6, 318 | Height: 0.7, 319 | }, 320 | 52: { 321 | Id: 52, 322 | Name: "painting", 323 | Width: 0.5, 324 | Height: 0.5, 325 | }, 326 | 53: { 327 | Id: 53, 328 | Name: "panda", 329 | Width: 1.3, 330 | Height: 1.25, 331 | }, 332 | 54: { 333 | Id: 54, 334 | Name: "parrot", 335 | Width: 0.5, 336 | Height: 0.9, 337 | }, 338 | 55: { 339 | Id: 55, 340 | Name: "pig", 341 | Width: 0.9, 342 | Height: 0.9, 343 | }, 344 | 56: { 345 | Id: 56, 346 | Name: "pufferfish", 347 | Width: 0.7, 348 | Height: 0.7, 349 | }, 350 | 57: { 351 | Id: 57, 352 | Name: "zombie_pigman", 353 | Width: 0.6, 354 | Height: 1.95, 355 | }, 356 | 58: { 357 | Id: 58, 358 | Name: "polar_bear", 359 | Width: 1.4, 360 | Height: 1.4, 361 | }, 362 | 59: { 363 | Id: 59, 364 | Name: "tnt", 365 | Width: 0.98, 366 | Height: 0.98, 367 | }, 368 | 60: { 369 | Id: 60, 370 | Name: "rabbit", 371 | Width: 0.4, 372 | Height: 0.5, 373 | }, 374 | 61: { 375 | Id: 61, 376 | Name: "salmon", 377 | Width: 0.7, 378 | Height: 0.4, 379 | }, 380 | 62: { 381 | Id: 62, 382 | Name: "sheep", 383 | Width: 0.9, 384 | Height: 1.3, 385 | }, 386 | 63: { 387 | Id: 63, 388 | Name: "shulker", 389 | Width: 1, 390 | Height: 1, 391 | }, 392 | 64: { 393 | Id: 64, 394 | Name: "shulker_bullet", 395 | Width: 0.3125, 396 | Height: 0.3125, 397 | }, 398 | 65: { 399 | Id: 65, 400 | Name: "silverfish", 401 | Width: 0.4, 402 | Height: 0.3, 403 | }, 404 | 66: { 405 | Id: 66, 406 | Name: "skeleton", 407 | Width: 0.6, 408 | Height: 1.99, 409 | }, 410 | 67: { 411 | Id: 67, 412 | Name: "skeleton_horse", 413 | Width: 1.39648, 414 | Height: 1.6, 415 | }, 416 | 68: { 417 | Id: 68, 418 | Name: "slime", 419 | Width: 2.04, 420 | Height: 2.04, 421 | }, 422 | 69: { 423 | Id: 69, 424 | Name: "small_fireball", 425 | Width: 0.3125, 426 | Height: 0.3125, 427 | }, 428 | 70: { 429 | Id: 70, 430 | Name: "snow_golem", 431 | Width: 0.7, 432 | Height: 1.9, 433 | }, 434 | 71: { 435 | Id: 71, 436 | Name: "snowball", 437 | Width: 0.25, 438 | Height: 0.25, 439 | }, 440 | 72: { 441 | Id: 72, 442 | Name: "spectral_arrow", 443 | Width: 0.5, 444 | Height: 0.5, 445 | }, 446 | 73: { 447 | Id: 73, 448 | Name: "spider", 449 | Width: 1.4, 450 | Height: 0.9, 451 | }, 452 | 74: { 453 | Id: 74, 454 | Name: "squid", 455 | Width: 0.8, 456 | Height: 0.8, 457 | }, 458 | 75: { 459 | Id: 75, 460 | Name: "stray", 461 | Width: 0.6, 462 | Height: 1.99, 463 | }, 464 | 76: { 465 | Id: 76, 466 | Name: "trader_llama", 467 | Width: 0.9, 468 | Height: 1.87, 469 | }, 470 | 77: { 471 | Id: 77, 472 | Name: "tropical_fish", 473 | Width: 0.5, 474 | Height: 0.4, 475 | }, 476 | 78: { 477 | Id: 78, 478 | Name: "turtle", 479 | Width: 1.2, 480 | Height: 0.4, 481 | }, 482 | 79: { 483 | Id: 79, 484 | Name: "egg", 485 | Width: 0.25, 486 | Height: 0.25, 487 | }, 488 | 80: { 489 | Id: 80, 490 | Name: "ender_pearl", 491 | Width: 0.25, 492 | Height: 0.25, 493 | }, 494 | 81: { 495 | Id: 81, 496 | Name: "experience_bottle", 497 | Width: 0.25, 498 | Height: 0.25, 499 | }, 500 | 82: { 501 | Id: 82, 502 | Name: "potion", 503 | Width: 0.25, 504 | Height: 0.25, 505 | }, 506 | 83: { 507 | Id: 83, 508 | Name: "trident", 509 | Width: 0.5, 510 | Height: 0.5, 511 | }, 512 | 84: { 513 | Id: 84, 514 | Name: "vex", 515 | Width: 0.4, 516 | Height: 0.8, 517 | }, 518 | 85: { 519 | Id: 85, 520 | Name: "villager", 521 | Width: 0.6, 522 | Height: 1.95, 523 | }, 524 | 86: { 525 | Id: 86, 526 | Name: "iron_golem", 527 | Width: 1.4, 528 | Height: 2.7, 529 | }, 530 | 87: { 531 | Id: 87, 532 | Name: "vindicator", 533 | Width: 0.6, 534 | Height: 1.95, 535 | }, 536 | 88: { 537 | Id: 88, 538 | Name: "pillager", 539 | Width: 0.6, 540 | Height: 1.95, 541 | }, 542 | 89: { 543 | Id: 89, 544 | Name: "wandering_trader", 545 | Width: 0.6, 546 | Height: 1.95, 547 | }, 548 | 90: { 549 | Id: 90, 550 | Name: "witch", 551 | Width: 0.6, 552 | Height: 1.95, 553 | }, 554 | 91: { 555 | Id: 91, 556 | Name: "wither", 557 | Width: 0.9, 558 | Height: 3.5, 559 | }, 560 | 92: { 561 | Id: 92, 562 | Name: "wither_skeleton", 563 | Width: 0.7, 564 | Height: 2.4, 565 | }, 566 | 93: { 567 | Id: 93, 568 | Name: "wither_skull", 569 | Width: 0.3125, 570 | Height: 0.3125, 571 | }, 572 | 94: { 573 | Id: 94, 574 | Name: "wolf", 575 | Width: 0.6, 576 | Height: 0.85, 577 | }, 578 | 95: { 579 | Id: 95, 580 | Name: "zombie", 581 | Width: 0.6, 582 | Height: 1.95, 583 | }, 584 | 96: { 585 | Id: 96, 586 | Name: "zombie_horse", 587 | Width: 1.39648, 588 | Height: 1.6, 589 | }, 590 | 97: { 591 | Id: 97, 592 | Name: "zombie_villager", 593 | Width: 0.6, 594 | Height: 1.95, 595 | }, 596 | 98: { 597 | Id: 98, 598 | Name: "phantom", 599 | Width: 0.9, 600 | Height: 0.5, 601 | }, 602 | 99: { 603 | Id: 99, 604 | Name: "ravager", 605 | Width: 1.95, 606 | Height: 2.2, 607 | }, 608 | 100: { 609 | Id: 100, 610 | Name: "lightning_bolt", 611 | Width: 0, 612 | Height: 0, 613 | }, 614 | 101: { 615 | Id: 101, 616 | Name: "player", 617 | Width: 0.6, 618 | Height: 1.8, 619 | }, 620 | 102: { 621 | Id: 102, 622 | Name: "fishing_bobber", 623 | Width: 0.25, 624 | Height: 0.25, 625 | }, 626 | } 627 | 628 | var byName = map[string]*Type{ 629 | "area_effect_cloud": &types[0], 630 | "armor_stand": &types[1], 631 | "arrow": &types[2], 632 | "bat": &types[3], 633 | "bee": &types[4], 634 | "blaze": &types[5], 635 | "boat": &types[6], 636 | "cat": &types[7], 637 | "cave_spider": &types[8], 638 | "chicken": &types[9], 639 | "cod": &types[10], 640 | "cow": &types[11], 641 | "creeper": &types[12], 642 | "donkey": &types[13], 643 | "dolphin": &types[14], 644 | "dragon_fireball": &types[15], 645 | "drowned": &types[16], 646 | "elder_guardian": &types[17], 647 | "end_crystal": &types[18], 648 | "ender_dragon": &types[19], 649 | "enderman": &types[20], 650 | "endermite": &types[21], 651 | "evoker_fangs": &types[22], 652 | "evoker": &types[23], 653 | "experience_orb": &types[24], 654 | "eye_of_ender": &types[25], 655 | "falling_block": &types[26], 656 | "firework_rocket": &types[27], 657 | "fox": &types[28], 658 | "ghast": &types[29], 659 | "giant": &types[30], 660 | "guardian": &types[31], 661 | "horse": &types[32], 662 | "husk": &types[33], 663 | "illusioner": &types[34], 664 | "item": &types[35], 665 | "item_frame": &types[36], 666 | "fireball": &types[37], 667 | "leash_knot": &types[38], 668 | "llama": &types[39], 669 | "llama_spit": &types[40], 670 | "magma_cube": &types[41], 671 | "minecart": &types[42], 672 | "chest_minecart": &types[43], 673 | "command_block_minecart": &types[44], 674 | "furnace_minecart": &types[45], 675 | "hopper_minecart": &types[46], 676 | "spawner_minecart": &types[47], 677 | "tnt_minecart": &types[48], 678 | "mule": &types[49], 679 | "mooshroom": &types[50], 680 | "ocelot": &types[51], 681 | "painting": &types[52], 682 | "panda": &types[53], 683 | "parrot": &types[54], 684 | "pig": &types[55], 685 | "pufferfish": &types[56], 686 | "zombie_pigman": &types[57], 687 | "polar_bear": &types[58], 688 | "tnt": &types[59], 689 | "rabbit": &types[60], 690 | "salmon": &types[61], 691 | "sheep": &types[62], 692 | "shulker": &types[63], 693 | "shulker_bullet": &types[64], 694 | "silverfish": &types[65], 695 | "skeleton": &types[66], 696 | "skeleton_horse": &types[67], 697 | "slime": &types[68], 698 | "small_fireball": &types[69], 699 | "snow_golem": &types[70], 700 | "snowball": &types[71], 701 | "spectral_arrow": &types[72], 702 | "spider": &types[73], 703 | "squid": &types[74], 704 | "stray": &types[75], 705 | "trader_llama": &types[76], 706 | "tropical_fish": &types[77], 707 | "turtle": &types[78], 708 | "egg": &types[79], 709 | "ender_pearl": &types[80], 710 | "experience_bottle": &types[81], 711 | "potion": &types[82], 712 | "trident": &types[83], 713 | "vex": &types[84], 714 | "villager": &types[85], 715 | "iron_golem": &types[86], 716 | "vindicator": &types[87], 717 | "pillager": &types[88], 718 | "wandering_trader": &types[89], 719 | "witch": &types[90], 720 | "wither": &types[91], 721 | "wither_skeleton": &types[92], 722 | "wither_skull": &types[93], 723 | "wolf": &types[94], 724 | "zombie": &types[95], 725 | "zombie_horse": &types[96], 726 | "zombie_villager": &types[97], 727 | "phantom": &types[98], 728 | "ravager": &types[99], 729 | "lightning_bolt": &types[100], 730 | "player": &types[101], 731 | "fishing_bobber": &types[102], 732 | } 733 | -------------------------------------------------------------------------------- /minecraft/entity/entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/itay2805/mcserver/math" 6 | "github.com/itay2805/mcserver/minecraft" 7 | "github.com/itay2805/mcserver/minecraft/proto/play" 8 | ) 9 | 10 | type Type struct { 11 | Id int 12 | Name string 13 | Width float64 14 | Height float64 15 | } 16 | 17 | 18 | type IEntity interface { 19 | math.Spatial 20 | GetEntity() *Entity 21 | UpdateBounds() 22 | } 23 | 24 | const ( 25 | isOnfire = 0x1 26 | isCrouching = 0x2 27 | isSprinting = 0x8 28 | isSwimming = 0x10 29 | isInvisible = 0x20 30 | hasGlowingEffect = 0x40 31 | isFlyingWithElytra = 0x80 32 | ) 33 | 34 | type Entity struct { 35 | // the entity type info 36 | Type *Type 37 | 38 | // Identifiers 39 | // per-runtime 40 | EID int32 41 | // persistent 42 | UUID uuid.UUID 43 | 44 | // the entity position 45 | Moved bool 46 | Rotated bool 47 | OnGroundChanged bool 48 | 49 | PrevPosition math.Point 50 | Position math.Point 51 | // TODO: PositionDelta 52 | Velocity math.Point 53 | Yaw, Pitch, HeadYaw minecraft.Angle 54 | OnGround bool 55 | 56 | // flags 57 | OnFire bool 58 | Sprinting bool 59 | Invisible bool 60 | Glowing bool 61 | Pose minecraft.Pose 62 | 63 | // The metadata of the entity 64 | // has changed 65 | MetadataChanged bool 66 | 67 | // the bounds of the entity 68 | bounds *math.Rect 69 | 70 | // The entity equipment 71 | Equipment [6]*play.Slot 72 | EquipmentChanged int 73 | 74 | // The animation to play 75 | Animation byte 76 | } 77 | 78 | func (e *Entity) GetFacing() minecraft.Face { 79 | // 225-256 or 0-31 80 | if 225 <= e.HeadYaw || e.HeadYaw <= 31 { 81 | return minecraft.FaceSouth 82 | } else if 161 <= e.HeadYaw && e.HeadYaw <= 224 { 83 | return minecraft.FaceEast 84 | } else if 97 <= e.HeadYaw && e.HeadYaw <= 160 { 85 | return minecraft.FaceNorth 86 | } else { 87 | return minecraft.FaceWest 88 | } 89 | } 90 | 91 | func (e *Entity) UpdateBounds() { 92 | e.bounds = math.NewRect( 93 | math.NewPoint(e.Position.X() - (e.Type.Width / 2), e.Position.Y(), e.Position.Z() - (e.Type.Width / 2)), 94 | [3]float64{ 95 | e.Type.Width, 96 | e.Type.Height, 97 | e.Type.Width, 98 | }, 99 | ) 100 | } 101 | 102 | func (e *Entity) Bounds() *math.Rect { 103 | return e.bounds 104 | } 105 | 106 | func (e *Entity) GetEntity() *Entity { 107 | return e 108 | } 109 | 110 | func (e *Entity) WriteMetadata(writer *minecraft.EntityMetadataWriter) { 111 | val := byte(0) 112 | 113 | // flags 114 | if e.OnFire { 115 | val |= isOnfire 116 | } 117 | if e.Sprinting { 118 | val |= isSprinting 119 | } 120 | if e.Glowing { 121 | val |= hasGlowingEffect 122 | } 123 | 124 | writer.WriteByte(0, val) 125 | writer.WritePose(6, e.Pose) 126 | } 127 | 128 | func GetEntityTypeByName(name string) { 129 | 130 | } 131 | -------------------------------------------------------------------------------- /minecraft/entity/living.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "github.com/itay2805/mcserver/minecraft" 4 | 5 | type Living struct { 6 | Entity 7 | 8 | // active hand 9 | IsHandActive bool 10 | OffhandActive bool 11 | } 12 | 13 | func (p *Living) GetEntity() *Entity { 14 | return &p.Entity 15 | } 16 | 17 | func (p *Living) WriteMetadata(writer *minecraft.EntityMetadataWriter) { 18 | p.Entity.WriteMetadata(writer) 19 | 20 | val := byte(0) 21 | 22 | if p.IsHandActive { 23 | val |= 0x1 24 | } 25 | 26 | if p.OffhandActive { 27 | val |= 0x2 28 | } 29 | 30 | writer.WriteByte(7, val) 31 | } 32 | -------------------------------------------------------------------------------- /minecraft/entity/player.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/itay2805/mcserver/math" 6 | "github.com/itay2805/mcserver/minecraft" 7 | ) 8 | 9 | const ( 10 | HandLeft = 0 11 | HandRight = 1 12 | ) 13 | 14 | type Player struct { 15 | Living 16 | 17 | // username of the player 18 | Username string 19 | 20 | // visual 21 | SkinMask byte 22 | MainHand byte 23 | 24 | // Action related 25 | Flying bool 26 | 27 | // The stats of the player 28 | Health float32 29 | Food float32 30 | } 31 | 32 | // The player entity type 33 | var playerType = byName["player"] 34 | 35 | func NewPlayer(username string, uuid uuid.UUID) *Player { 36 | return &Player{ 37 | Living: Living{ 38 | Entity: Entity{ 39 | Type: playerType, 40 | EID: 0, // TODO: generate 41 | UUID: uuid, 42 | Moved: false, 43 | Rotated: false, 44 | OnGroundChanged: false, 45 | Position: math.Point{0, 0, 0}, 46 | Velocity: math.Point{0, 0, 0}, 47 | Yaw: 0, 48 | Pitch: 0, 49 | HeadYaw: 0, 50 | OnGround: true, 51 | OnFire: false, 52 | Sprinting: false, 53 | Invisible: false, 54 | Glowing: false, 55 | Pose: minecraft.PoseStanding, 56 | MetadataChanged: false, 57 | bounds: nil, 58 | }, 59 | IsHandActive: false, 60 | OffhandActive: false, 61 | }, 62 | SkinMask: 0, 63 | MainHand: 0, 64 | Username: username, 65 | Health: 20, 66 | Food: 20, 67 | } 68 | } 69 | 70 | func (p *Player) WriteMetadata(writer *minecraft.EntityMetadataWriter) { 71 | p.Living.WriteMetadata(writer) 72 | writer.WriteByte(16, p.SkinMask) 73 | writer.WriteByte(17, p.MainHand) 74 | } 75 | -------------------------------------------------------------------------------- /minecraft/item/item.go: -------------------------------------------------------------------------------- 1 | package item 2 | 3 | import "fmt" 4 | 5 | type Item struct { 6 | ID int 7 | Name string 8 | StackSize int 9 | } 10 | 11 | func (item *Item) String() string { 12 | return fmt.Sprintf("Item{ Name: \"%s\" }", item.Name) 13 | } 14 | 15 | func GetById(id int) *Item { 16 | return items[id] 17 | } 18 | -------------------------------------------------------------------------------- /minecraft/nbt.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "math" 12 | "reflect" 13 | "strings" 14 | "sync" 15 | ) 16 | 17 | const ( 18 | nbtTagEnd = 0 19 | nbtTagByte = 1 20 | nbtTagShort = 2 21 | nbtTagInt = 3 22 | nbtTagLong = 4 23 | nbtTagFloat = 5 24 | nbtTagDouble = 6 25 | nbtTagByteArray = 7 26 | nbtTagString = 8 27 | nbtTagList = 9 28 | nbtTagCompound = 10 29 | nbtTagIntArray = 11 30 | nbtTagLongArray = 12 31 | ) 32 | 33 | // stream writer (easier to use in some cases) 34 | 35 | type NbtWriter struct { 36 | w *Writer 37 | hierarchy []uint8 38 | listSizeStack []int 39 | listSizeOffsetStack []int 40 | } 41 | 42 | func (writer *NbtWriter) emitString(val string) { 43 | writer.w.WriteShort(int16(len(val))) 44 | writer.w.WriteBytes([]byte(val)) 45 | } 46 | 47 | func (writer *NbtWriter) pushListElement() { 48 | if len(writer.hierarchy) != 0 { 49 | if writer.hierarchy[len(writer.hierarchy) - 1] == nbtTagList { 50 | writer.listSizeStack[len(writer.listSizeStack) - 1]++ 51 | } 52 | } 53 | } 54 | 55 | func (writer *NbtWriter) emitTagHeader(t uint8, name string) { 56 | if len(writer.hierarchy) == 0 || writer.hierarchy[len(writer.hierarchy) - 1] != nbtTagEnd { 57 | writer.w.WriteByte(t) 58 | writer.emitString(name) 59 | } 60 | writer.pushListElement() 61 | } 62 | 63 | func (writer *NbtWriter) PushByte(val int8, name string) { 64 | writer.emitTagHeader(nbtTagByte, name) 65 | writer.w.WriteByte(byte(val)) 66 | } 67 | 68 | func (writer *NbtWriter) PushBool(val bool, name string) { 69 | writer.emitTagHeader(nbtTagByte, name) 70 | if val { 71 | writer.w.WriteByte(1) 72 | } else { 73 | writer.w.WriteByte(0) 74 | } 75 | } 76 | 77 | 78 | func (writer *NbtWriter) PushShort(val int16, name string) { 79 | writer.emitTagHeader(nbtTagByte, name) 80 | writer.w.WriteShort(val) 81 | } 82 | 83 | func (writer *NbtWriter) PushInt(val int32, name string) { 84 | writer.emitTagHeader(nbtTagInt, name) 85 | writer.w.WriteInt(val) 86 | } 87 | 88 | func (writer *NbtWriter) PushLong(val int64, name string) { 89 | writer.emitTagHeader(nbtTagLong, name) 90 | writer.w.WriteLong(val) 91 | } 92 | 93 | func (writer *NbtWriter) PushFloat(val float32, name string) { 94 | writer.emitTagHeader(nbtTagFloat, name) 95 | writer.w.WriteFloat(val) 96 | } 97 | 98 | func (writer *NbtWriter) PushDouble(val float64, name string) { 99 | writer.emitTagHeader(nbtTagDouble, name) 100 | writer.w.WriteDouble(val) 101 | } 102 | 103 | func (writer *NbtWriter) PushByteArray(data []byte, name string) { 104 | writer.emitTagHeader(nbtTagByteArray, name) 105 | writer.w.WriteInt(int32(len(data))) 106 | writer.w.WriteBytes(data) 107 | } 108 | 109 | func (writer *NbtWriter) PushIntArray(data []int32, name string) { 110 | writer.emitTagHeader(nbtTagIntArray, name) 111 | writer.w.WriteInt(int32(len(data))) 112 | for _, val := range data { 113 | writer.w.WriteInt(val) 114 | } 115 | } 116 | 117 | func (writer *NbtWriter) PushLongArray(data []int64, name string) { 118 | writer.emitTagHeader(nbtTagLongArray, name) 119 | writer.w.WriteInt(int32(len(data))) 120 | for _, val := range data { 121 | writer.w.WriteLong(val) 122 | } 123 | } 124 | 125 | func (writer *NbtWriter) PushString(val string, name string) { 126 | writer.emitTagHeader(nbtTagString, name) 127 | writer.emitString(val) 128 | } 129 | 130 | func (writer *NbtWriter) StartCompound(name string) { 131 | writer.emitTagHeader(nbtTagCompound, name) 132 | writer.hierarchy = append(writer.hierarchy, nbtTagCompound) 133 | } 134 | 135 | func (writer *NbtWriter) EndCompound() { 136 | writer.w.WriteByte(nbtTagEnd) 137 | writer.hierarchy = writer.hierarchy[:len(writer.hierarchy) - 1] 138 | } 139 | 140 | // TODO: List 141 | 142 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 143 | // nbt marshal and unmarshal 144 | // take from https://github.com/Tnze/go-mc/tree/master/nbt 145 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 146 | 147 | func NbtMarshal(w io.Writer, v interface{}) error { 148 | return NbtNewEncoder(w).Encode(v) 149 | } 150 | 151 | func NbtMarshalCompound(w io.Writer, v interface{}, rootTagName string) error { 152 | enc := NbtNewEncoder(w) 153 | val := reflect.ValueOf(v) 154 | if val.Kind() != reflect.Struct { 155 | log.Panicln("Must be struct!") 156 | } 157 | return enc.marshal(val, nbtTagCompound, rootTagName) 158 | } 159 | 160 | type NbtEncoder struct { 161 | w io.Writer 162 | } 163 | 164 | func NbtNewEncoder(w io.Writer) *NbtEncoder { 165 | return &NbtEncoder{w: w} 166 | } 167 | 168 | func (e *NbtEncoder) Encode(v interface{}) error { 169 | val := reflect.ValueOf(v) 170 | return e.marshal(val, getTagType(val.Type()), "") 171 | } 172 | 173 | func (e *NbtEncoder) marshal(val reflect.Value, tagType byte, tagName string) error { 174 | if err := e.writeHeader(val, tagType, tagName); err != nil { 175 | return err 176 | } 177 | return e.writeValue(val, tagType) 178 | } 179 | 180 | func (e *NbtEncoder) writeHeader(val reflect.Value, tagType byte, tagName string) (err error) { 181 | if tagType == nbtTagList { 182 | eleType := getTagType(val.Type().Elem()) 183 | err = e.writeListHeader(eleType, tagName, val.Len()) 184 | } else { 185 | err = e.writeTag(tagType, tagName) 186 | } 187 | return err 188 | } 189 | 190 | func (e *NbtEncoder) writeValue(val reflect.Value, tagType byte) error { 191 | switch tagType { 192 | default: 193 | return errors.New("unsupported type " + val.Type().Kind().String()) 194 | case nbtTagByte: 195 | _, err := e.w.Write([]byte{byte(val.Uint())}) 196 | return err 197 | case nbtTagShort: 198 | return e.writeInt16(int16(val.Int())) 199 | case nbtTagInt: 200 | return e.writeInt32(int32(val.Int())) 201 | case nbtTagFloat: 202 | return e.writeInt32(int32(math.Float32bits(float32(val.Float())))) 203 | case nbtTagLong: 204 | return e.writeInt64(val.Int()) 205 | case nbtTagDouble: 206 | return e.writeInt64(int64(math.Float64bits(val.Float()))) 207 | case nbtTagByteArray, nbtTagIntArray, nbtTagLongArray: 208 | n := val.Len() 209 | if err := e.writeInt32(int32(n)); err != nil { 210 | return err 211 | } 212 | 213 | if tagType == nbtTagByteArray { 214 | _, err := e.w.Write(val.Bytes()) 215 | return err 216 | } else { 217 | for i := 0; i < n; i++ { 218 | v := val.Index(i).Int() 219 | 220 | var err error 221 | if tagType == nbtTagIntArray { 222 | err = e.writeInt32(int32(v)) 223 | } else if tagType == nbtTagLongArray { 224 | err = e.writeInt64(v) 225 | } 226 | if err != nil { 227 | return err 228 | } 229 | } 230 | } 231 | 232 | case nbtTagList: 233 | for i := 0; i < val.Len(); i++ { 234 | arrVal := val.Index(i) 235 | err := e.writeValue(arrVal, getTagType(arrVal.Type())) 236 | if err != nil { 237 | return err 238 | } 239 | } 240 | 241 | case nbtTagString: 242 | if err := e.writeInt16(int16(val.Len())); err != nil { 243 | return err 244 | } 245 | _, err := e.w.Write([]byte(val.String())) 246 | return err 247 | 248 | case nbtTagCompound: 249 | if val.Kind() == reflect.Map { 250 | for _, key := range val.MapKeys() { 251 | value := val.MapIndex(key) 252 | err := e.marshal(value, getTagType(value.Type()), key.Interface().(string)) 253 | if err != nil { 254 | return err 255 | } 256 | } 257 | } else { 258 | if val.Kind() == reflect.Interface { 259 | val = reflect.ValueOf(val.Interface()) 260 | } 261 | 262 | n := val.NumField() 263 | for i := 0; i < n; i++ { 264 | f := val.Type().Field(i) 265 | tag := f.Tag.Get("nbt") 266 | if (f.PkgPath != "" && !f.Anonymous) || tag == "-" { 267 | continue // Private field 268 | } 269 | 270 | tagProps := parseTag(f, tag) 271 | err := e.marshal(val.Field(i), tagProps.Type, tagProps.Name) 272 | if err != nil { 273 | return err 274 | } 275 | } 276 | } 277 | 278 | _, err := e.w.Write([]byte{nbtTagEnd}) 279 | return err 280 | } 281 | return nil 282 | } 283 | 284 | func getTagType(vk reflect.Type) byte { 285 | switch vk.Kind() { 286 | case reflect.Uint8: 287 | return nbtTagByte 288 | case reflect.Int16, reflect.Uint16: 289 | return nbtTagShort 290 | case reflect.Int32, reflect.Uint32: 291 | return nbtTagInt 292 | case reflect.Float32: 293 | return nbtTagFloat 294 | case reflect.Int64, reflect.Uint64: 295 | return nbtTagLong 296 | case reflect.Float64: 297 | return nbtTagDouble 298 | case reflect.String: 299 | return nbtTagString 300 | case reflect.Struct, reflect.Interface: 301 | return nbtTagCompound 302 | case reflect.Array, reflect.Slice: 303 | switch vk.Elem().Kind() { 304 | case reflect.Uint8: // Special types for these values 305 | return nbtTagByteArray 306 | case reflect.Int32: 307 | return nbtTagIntArray 308 | case reflect.Int64: 309 | return nbtTagLongArray 310 | default: 311 | return nbtTagList 312 | } 313 | case reflect.Map: 314 | return nbtTagCompound 315 | default: 316 | log.Panicln("Invalid type", vk) 317 | } 318 | return 0 319 | } 320 | 321 | type tagProps struct { 322 | Name string 323 | Type byte 324 | } 325 | 326 | func isArrayTag(ty byte) bool { 327 | return ty == nbtTagByteArray || ty == nbtTagIntArray || ty == nbtTagLongArray 328 | } 329 | 330 | func parseTag(f reflect.StructField, tagName string) tagProps { 331 | result := tagProps{} 332 | result.Name = tagName 333 | if result.Name == "" { 334 | result.Name = f.Name 335 | } 336 | 337 | nbtType := f.Tag.Get("nbt_type") 338 | result.Type = getTagType(f.Type) 339 | if strings.Contains(nbtType, "list") { 340 | if isArrayTag(result.Type) { 341 | result.Type = nbtTagList // for expanding the array to a standard list 342 | } else { 343 | panic("list is only supported for array types (byte, int, long)") 344 | } 345 | } 346 | 347 | return result 348 | } 349 | 350 | func (e *NbtEncoder) writeTag(tagType byte, tagName string) error { 351 | if _, err := e.w.Write([]byte{tagType}); err != nil { 352 | return err 353 | } 354 | bName := []byte(tagName) 355 | if err := e.writeInt16(int16(len(bName))); err != nil { 356 | return err 357 | } 358 | _, err := e.w.Write(bName) 359 | return err 360 | } 361 | 362 | func (e *NbtEncoder) writeListHeader(elementType byte, tagName string, n int) (err error) { 363 | if err = e.writeTag(nbtTagList, tagName); err != nil { 364 | return 365 | } 366 | if _, err = e.w.Write([]byte{elementType}); err != nil { 367 | return 368 | } 369 | // Write length of strings 370 | if err = e.writeInt32(int32(n)); err != nil { 371 | return 372 | } 373 | return nil 374 | } 375 | 376 | func (e *NbtEncoder) writeInt16(n int16) error { 377 | _, err := e.w.Write([]byte{byte(n >> 8), byte(n)}) 378 | return err 379 | } 380 | 381 | func (e *NbtEncoder) writeInt32(n int32) error { 382 | _, err := e.w.Write([]byte{byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n)}) 383 | return err 384 | } 385 | 386 | func (e *NbtEncoder) writeInt64(n int64) error { 387 | _, err := e.w.Write([]byte{ 388 | byte(n >> 56), byte(n >> 48), byte(n >> 40), byte(n >> 32), 389 | byte(n >> 24), byte(n >> 16), byte(n >> 8), byte(n)}) 390 | return err 391 | } 392 | 393 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 394 | 395 | type NbtDecoderReader = interface { 396 | io.ByteScanner 397 | io.Reader 398 | } 399 | type NbtDecoder struct { 400 | r NbtDecoderReader 401 | } 402 | 403 | func NewNbtDecoder(r io.Reader) *NbtDecoder { 404 | d := new(NbtDecoder) 405 | if br, ok := r.(NbtDecoderReader); ok { 406 | d.r = br 407 | } else { 408 | d.r = bufio.NewReader(r) 409 | } 410 | return d 411 | } 412 | 413 | func NbtUnmarshal(data []byte, v interface{}) error { 414 | return NewNbtDecoder(bytes.NewReader(data)).Decode(v) 415 | } 416 | 417 | func (d *NbtDecoder) Decode(v interface{}) error { 418 | val := reflect.ValueOf(v) 419 | if val.Kind() != reflect.Ptr { 420 | return errors.New("nbt: non-pointer passed to NbtUnmarshal") 421 | } 422 | 423 | //start read NBT 424 | tagType, tagName, err := d.readTag() 425 | if err != nil { 426 | return fmt.Errorf("nbt: %w", err) 427 | } 428 | 429 | if c := d.checkCompressed(tagType); c != "" { 430 | return fmt.Errorf("nbt: unknown Tag, maybe need %s", c) 431 | } 432 | 433 | err = d.unmarshal(val.Elem(), tagType, tagName) 434 | if err != nil { 435 | return fmt.Errorf("nbt: fail to decode tag %q: %w", tagName, err) 436 | } 437 | return nil 438 | } 439 | 440 | // check the first byte and return if it use compress 441 | func (d *NbtDecoder) checkCompressed(head byte) (compress string) { 442 | if head == 0x1f { //gzip 443 | compress = "gzip" 444 | } else if head == 0x78 { //zlib 445 | compress = "zlib" 446 | } 447 | return 448 | } 449 | 450 | // ErrEND error will be returned when reading a NBT with only Tag_End 451 | var ErrEND = errors.New("NBT with only Tag_End") 452 | 453 | type typeInfo struct { 454 | tagName string 455 | nameToIndex map[string]int 456 | } 457 | 458 | var tInfoMap sync.Map 459 | 460 | func getTypeInfo(typ reflect.Type) *typeInfo { 461 | if ti, ok := tInfoMap.Load(typ); ok { 462 | return ti.(*typeInfo) 463 | } 464 | 465 | tInfo := new(typeInfo) 466 | tInfo.nameToIndex = make(map[string]int) 467 | if typ.Kind() == reflect.Struct { 468 | n := typ.NumField() 469 | for i := 0; i < n; i++ { 470 | f := typ.Field(i) 471 | tag := f.Tag.Get("nbt") 472 | if (f.PkgPath != "" && !f.Anonymous) || tag == "-" { 473 | continue // Private field 474 | } 475 | 476 | tInfo.nameToIndex[tag] = i 477 | if _, ok := tInfo.nameToIndex[f.Name]; !ok { 478 | tInfo.nameToIndex[f.Name] = i 479 | } 480 | } 481 | } 482 | 483 | ti, _ := tInfoMap.LoadOrStore(typ, tInfo) 484 | return ti.(*typeInfo) 485 | } 486 | 487 | func (t *typeInfo) findIndexByName(name string) int { 488 | i, ok := t.nameToIndex[name] 489 | if !ok { 490 | return -1 491 | } 492 | return i 493 | } 494 | 495 | func (d *NbtDecoder) unmarshal(val reflect.Value, tagType byte, tagName string) error { 496 | switch tagType { 497 | default: 498 | return fmt.Errorf("unknown Tag 0x%02x", tagType) 499 | 500 | case nbtTagEnd: 501 | return ErrEND 502 | 503 | case nbtTagByte: 504 | value, err := d.r.ReadByte() 505 | if err != nil { 506 | return err 507 | } 508 | switch vk := val.Kind(); vk { 509 | default: 510 | return errors.New("cannot parse TagByte as " + vk.String()) 511 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 512 | val.SetInt(int64(value)) 513 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 514 | val.SetUint(uint64(value)) 515 | case reflect.Interface: 516 | val.Set(reflect.ValueOf(value)) 517 | } 518 | 519 | case nbtTagShort: 520 | value, err := d.readInt16() 521 | if err != nil { 522 | return err 523 | } 524 | switch vk := val.Kind(); vk { 525 | default: 526 | return errors.New("cannot parse TagShort as " + vk.String()) 527 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 528 | val.SetInt(int64(value)) 529 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: 530 | val.SetUint(uint64(value)) 531 | case reflect.Interface: 532 | val.Set(reflect.ValueOf(value)) 533 | } 534 | 535 | case nbtTagInt: 536 | value, err := d.readInt32() 537 | if err != nil { 538 | return err 539 | } 540 | switch vk := val.Kind(); vk { 541 | default: 542 | return errors.New("cannot parse TagInt as " + vk.String()) 543 | case reflect.Int, reflect.Int32, reflect.Int64: 544 | val.SetInt(int64(value)) 545 | case reflect.Uint, reflect.Uint32, reflect.Uint64: 546 | val.SetUint(uint64(value)) 547 | case reflect.Interface: 548 | val.Set(reflect.ValueOf(value)) 549 | } 550 | 551 | case nbtTagFloat: 552 | vInt, err := d.readInt32() 553 | if err != nil { 554 | return err 555 | } 556 | value := math.Float32frombits(uint32(vInt)) 557 | switch vk := val.Kind(); vk { 558 | default: 559 | return errors.New("cannot parse TagFloat as " + vk.String()) 560 | case reflect.Float32: 561 | val.Set(reflect.ValueOf(value)) 562 | case reflect.Float64: 563 | val.Set(reflect.ValueOf(float64(value))) 564 | case reflect.Interface: 565 | val.Set(reflect.ValueOf(value)) 566 | } 567 | 568 | case nbtTagLong: 569 | value, err := d.readInt64() 570 | if err != nil { 571 | return err 572 | } 573 | switch vk := val.Kind(); vk { 574 | default: 575 | return errors.New("cannot parse TagLong as " + vk.String()) 576 | case reflect.Int, reflect.Int64: 577 | val.SetInt(int64(value)) 578 | case reflect.Uint, reflect.Uint64: 579 | val.SetUint(uint64(value)) 580 | case reflect.Interface: 581 | val.Set(reflect.ValueOf(value)) 582 | } 583 | 584 | case nbtTagDouble: 585 | vInt, err := d.readInt64() 586 | if err != nil { 587 | return err 588 | } 589 | value := math.Float64frombits(uint64(vInt)) 590 | 591 | switch vk := val.Kind(); vk { 592 | default: 593 | return errors.New("cannot parse TagDouble as " + vk.String()) 594 | case reflect.Float64: 595 | val.Set(reflect.ValueOf(value)) 596 | case reflect.Interface: 597 | val.Set(reflect.ValueOf(value)) 598 | } 599 | 600 | case nbtTagString: 601 | s, err := d.readString() 602 | if err != nil { 603 | return err 604 | } 605 | switch vk := val.Kind(); vk { 606 | default: 607 | return errors.New("cannot parse TagString as " + vk.String()) 608 | case reflect.String: 609 | val.SetString(s) 610 | case reflect.Interface: 611 | val.Set(reflect.ValueOf(s)) 612 | } 613 | 614 | case nbtTagByteArray: 615 | aryLen, err := d.readInt32() 616 | if err != nil { 617 | return err 618 | } 619 | if aryLen < 0 { 620 | return errors.New("byte array len less than 0") 621 | } 622 | ba := make([]byte, aryLen) 623 | if _, err = io.ReadFull(d.r, ba); err != nil { 624 | return err 625 | } 626 | 627 | switch vt := val.Type(); { 628 | default: 629 | return errors.New("cannot parse TagByteArray to " + vt.String() + ", use []byte in this instance") 630 | case vt == reflect.TypeOf(ba): 631 | val.SetBytes(ba) 632 | case vt.Kind() == reflect.Interface: 633 | val.Set(reflect.ValueOf(ba)) 634 | } 635 | 636 | case nbtTagIntArray: 637 | aryLen, err := d.readInt32() 638 | if err != nil { 639 | return err 640 | } 641 | vt := val.Type() //receiver must be []int or []int32 642 | if vt.Kind() == reflect.Interface { 643 | vt = reflect.TypeOf([]int32{}) // pass 644 | } else if vt.Kind() != reflect.Slice { 645 | return errors.New("cannot parse TagIntArray to " + vt.String() + ", it must be a slice") 646 | } else if tk := val.Type().Elem().Kind(); tk != reflect.Int && tk != reflect.Int32 { 647 | return errors.New("cannot parse TagIntArray to " + vt.String()) 648 | } 649 | 650 | buf := reflect.MakeSlice(vt, int(aryLen), int(aryLen)) 651 | for i := 0; i < int(aryLen); i++ { 652 | value, err := d.readInt32() 653 | if err != nil { 654 | return err 655 | } 656 | buf.Index(i).SetInt(int64(value)) 657 | } 658 | val.Set(buf) 659 | 660 | case nbtTagLongArray: 661 | aryLen, err := d.readInt32() 662 | if err != nil { 663 | return err 664 | } 665 | vt := val.Type() //receiver must be []int or []int64 666 | if vt.Kind() == reflect.Interface { 667 | vt = reflect.TypeOf([]int64{}) // pass 668 | } else if vt.Kind() != reflect.Slice { 669 | return errors.New("cannot parse TagLongArray to " + vt.String() + ", it must be a slice") 670 | } else if val.Type().Elem().Kind() != reflect.Int64 { 671 | return errors.New("cannot parse TagLongArray to " + vt.String()) 672 | } 673 | 674 | buf := reflect.MakeSlice(vt, int(aryLen), int(aryLen)) 675 | for i := 0; i < int(aryLen); i++ { 676 | value, err := d.readInt64() 677 | if err != nil { 678 | return err 679 | } 680 | buf.Index(i).SetInt(value) 681 | } 682 | val.Set(buf) 683 | 684 | case nbtTagList: 685 | listType, err := d.r.ReadByte() 686 | if err != nil { 687 | return err 688 | } 689 | listLen, err := d.readInt32() 690 | if err != nil { 691 | return err 692 | } 693 | if listLen < 0 { 694 | return errors.New("list length less than 0") 695 | } 696 | 697 | // If we need parse TAG_List into slice, make a new with right length. 698 | // Otherwise if we need parse into array, we check if len(array) are enough. 699 | var buf reflect.Value 700 | vk := val.Kind() 701 | switch vk { 702 | default: 703 | return errors.New("cannot parse TagList as " + vk.String()) 704 | case reflect.Interface: 705 | buf = reflect.ValueOf(make([]interface{}, listLen)) 706 | case reflect.Slice: 707 | buf = reflect.MakeSlice(val.Type(), int(listLen), int(listLen)) 708 | case reflect.Array: 709 | if vl := val.Len(); vl < int(listLen) { 710 | return fmt.Errorf( 711 | "TagList %s has len %d, but array %v only has len %d", 712 | tagName, listLen, val.Type(), vl) 713 | } 714 | buf = val 715 | } 716 | for i := 0; i < int(listLen); i++ { 717 | if err := d.unmarshal(buf.Index(i), listType, ""); err != nil { 718 | return err 719 | } 720 | } 721 | 722 | if vk != reflect.Array { 723 | val.Set(buf) 724 | } 725 | 726 | case nbtTagCompound: 727 | switch vk := val.Kind(); vk { 728 | default: 729 | return errors.New("cannot parse TagCompound as " + vk.String()) 730 | case reflect.Struct: 731 | tinfo := getTypeInfo(val.Type()) 732 | for { 733 | tt, tn, err := d.readTag() 734 | if err != nil { 735 | return err 736 | } 737 | if tt == nbtTagEnd { 738 | break 739 | } 740 | field := tinfo.findIndexByName(tn) 741 | if field != -1 { 742 | err = d.unmarshal(val.Field(field), tt, tn) 743 | if err != nil { 744 | return fmt.Errorf("fail to decode tag %q: %w", tn, err) 745 | } 746 | } else { 747 | if err := d.rawRead(tt); err != nil { 748 | return err 749 | } 750 | } 751 | } 752 | case reflect.Map: 753 | if val.Type().Key().Kind() != reflect.String { 754 | return errors.New("cannot parse TagCompound as " + val.Type().String()) 755 | } 756 | if val.IsNil() { 757 | val.Set(reflect.MakeMap(val.Type())) 758 | } 759 | for { 760 | tt, tn, err := d.readTag() 761 | if err != nil { 762 | return err 763 | } 764 | if tt == nbtTagEnd { 765 | break 766 | } 767 | v := reflect.New(val.Type().Elem()) 768 | if err = d.unmarshal(v.Elem(), tt, tn); err != nil { 769 | return fmt.Errorf("fail to decode tag %q: %w", tn, err) 770 | } 771 | val.SetMapIndex(reflect.ValueOf(tn), v.Elem()) 772 | } 773 | case reflect.Interface: 774 | buf := make(map[string]interface{}) 775 | for { 776 | tt, tn, err := d.readTag() 777 | if err != nil { 778 | return err 779 | } 780 | if tt == nbtTagEnd { 781 | break 782 | } 783 | var value interface{} 784 | if err = d.unmarshal(reflect.ValueOf(&value).Elem(), tt, tn); err != nil { 785 | return fmt.Errorf("fail to decode tag %q: %w", tn, err) 786 | } 787 | buf[tn] = value 788 | } 789 | val.Set(reflect.ValueOf(buf)) 790 | } 791 | } 792 | 793 | return nil 794 | } 795 | 796 | func (d *NbtDecoder) rawRead(tagType byte) error { 797 | var buf [8]byte 798 | switch tagType { 799 | default: 800 | return fmt.Errorf("unknown to read 0x%02x", tagType) 801 | case nbtTagByte: 802 | _, err := d.r.ReadByte() 803 | return err 804 | case nbtTagString: 805 | _, err := d.readString() 806 | return err 807 | case nbtTagShort: 808 | _, err := io.ReadFull(d.r, buf[:2]) 809 | return err 810 | case nbtTagInt, nbtTagFloat: 811 | _, err := io.ReadFull(d.r, buf[:4]) 812 | return err 813 | case nbtTagLong, nbtTagDouble: 814 | _, err := io.ReadFull(d.r, buf[:8]) 815 | return err 816 | case nbtTagByteArray: 817 | aryLen, err := d.readInt32() 818 | if err != nil { 819 | return err 820 | } 821 | 822 | if _, err = io.CopyN(ioutil.Discard, d.r, int64(aryLen)); err != nil { 823 | return err 824 | } 825 | case nbtTagIntArray: 826 | aryLen, err := d.readInt32() 827 | if err != nil { 828 | return err 829 | } 830 | for i := 0; i < int(aryLen); i++ { 831 | if _, err := d.readInt32(); err != nil { 832 | return err 833 | } 834 | } 835 | 836 | case nbtTagLongArray: 837 | aryLen, err := d.readInt32() 838 | if err != nil { 839 | return err 840 | } 841 | for i := 0; i < int(aryLen); i++ { 842 | if _, err := d.readInt64(); err != nil { 843 | return err 844 | } 845 | } 846 | 847 | case nbtTagList: 848 | listType, err := d.r.ReadByte() 849 | if err != nil { 850 | return err 851 | } 852 | listLen, err := d.readInt32() 853 | if err != nil { 854 | return err 855 | } 856 | for i := 0; i < int(listLen); i++ { 857 | if err := d.rawRead(listType); err != nil { 858 | return err 859 | } 860 | } 861 | case nbtTagCompound: 862 | for { 863 | tt, _, err := d.readTag() 864 | if err != nil { 865 | return err 866 | } 867 | if tt == nbtTagEnd { 868 | break 869 | } 870 | err = d.rawRead(tt) 871 | if err != nil { 872 | return err 873 | } 874 | } 875 | } 876 | return nil 877 | } 878 | 879 | func (d *NbtDecoder) readTag() (tagType byte, tagName string, err error) { 880 | tagType, err = d.r.ReadByte() 881 | if err != nil { 882 | return 883 | } 884 | 885 | if tagType != nbtTagEnd { //Read Tag 886 | tagName, err = d.readString() 887 | } 888 | return 889 | } 890 | 891 | func (d *NbtDecoder) readInt16() (int16, error) { 892 | var data [2]byte 893 | _, err := io.ReadFull(d.r, data[:]) 894 | return int16(data[0])<<8 | int16(data[1]), err 895 | } 896 | 897 | func (d *NbtDecoder) readInt32() (int32, error) { 898 | var data [4]byte 899 | _, err := io.ReadFull(d.r, data[:]) 900 | return int32(data[0])<<24 | int32(data[1])<<16 | 901 | int32(data[2])<<8 | int32(data[3]), err 902 | } 903 | 904 | func (d *NbtDecoder) readInt64() (int64, error) { 905 | var data [8]byte 906 | _, err := io.ReadFull(d.r, data[:]) 907 | return int64(data[0])<<56 | int64(data[1])<<48 | 908 | int64(data[2])<<40 | int64(data[3])<<32 | 909 | int64(data[4])<<24 | int64(data[5])<<16 | 910 | int64(data[6])<<8 | int64(data[7]), err 911 | } 912 | 913 | func (d *NbtDecoder) readString() (string, error) { 914 | length, err := d.readInt16() 915 | if err != nil { 916 | return "", err 917 | } else if length < 0 { 918 | return "", errors.New("string length less than 0") 919 | } 920 | 921 | var str string 922 | if length > 0 { 923 | buf := make([]byte, length) 924 | _, err = io.ReadFull(d.r, buf) 925 | str = string(buf) 926 | } 927 | return str, err 928 | } 929 | -------------------------------------------------------------------------------- /minecraft/proto/login/packets.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/itay2805/mcserver/minecraft" 6 | ) 7 | 8 | type Disconnect struct { 9 | Reason minecraft.Chat 10 | } 11 | 12 | func (r Disconnect) Encode(writer *minecraft.Writer) { 13 | writer.WriteVarint(0x00) 14 | writer.WriteChat(r.Reason) 15 | } 16 | 17 | type EncryptionRequest struct { 18 | ServerId string 19 | PublicKey []byte 20 | VerifyToken []byte 21 | } 22 | 23 | func (r EncryptionRequest) Encode(writer *minecraft.Writer) { 24 | writer.WriteVarint(0x01) 25 | writer.WriteString(r.ServerId) 26 | writer.WriteVarint(int32(len(r.PublicKey))) 27 | writer.WriteBytes(r.PublicKey) 28 | writer.WriteVarint(int32(len(r.VerifyToken))) 29 | writer.WriteBytes(r.VerifyToken) 30 | } 31 | 32 | type LoginSuccess struct { 33 | Uuid uuid.UUID 34 | Username string 35 | } 36 | 37 | func (r LoginSuccess) Encode(writer *minecraft.Writer) { 38 | writer.WriteVarint(0x02) 39 | writer.WriteString(r.Uuid.String()) 40 | writer.WriteString(r.Username) 41 | } 42 | 43 | type SetCompression struct { 44 | Threshold int32 45 | } 46 | 47 | func (r SetCompression) Encode(writer *minecraft.Writer) { 48 | writer.WriteVarint(0x03) 49 | writer.WriteVarint(r.Threshold) 50 | } -------------------------------------------------------------------------------- /minecraft/proto/play/entity.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/itay2805/mcserver/minecraft" 6 | ) 7 | 8 | // TODO: Spawn Entity 9 | 10 | // TODO: Spawn Experience Orn 11 | 12 | // TODO: Spawn Weather Entity 13 | 14 | // TODO: Spawn Living Entity 15 | 16 | // TODO: Spawn Painting 17 | 18 | type SpawnPlayer struct { 19 | EntityID int32 20 | UUID uuid.UUID 21 | X float64 22 | Y float64 23 | Z float64 24 | Yaw minecraft.Angle 25 | Pitch minecraft.Angle 26 | } 27 | 28 | func (r SpawnPlayer) Encode(writer *minecraft.Writer) { 29 | writer.WriteVarint(0x05) 30 | writer.WriteVarint(r.EntityID) 31 | writer.WriteUUID(r.UUID) 32 | writer.WriteDouble(r.X) 33 | writer.WriteDouble(r.Y) 34 | writer.WriteDouble(r.Z) 35 | writer.WriteAngle(r.Yaw) 36 | writer.WriteAngle(r.Pitch) 37 | } 38 | 39 | const ( 40 | AnimationSwingMainHand = byte(iota) 41 | AnimationTakeDamage 42 | AnimationLeaveBed 43 | AnimationSwingOffhand 44 | AnimationCriticalEffect 45 | AnimationMagicCriticalEffect 46 | AnimationNone 47 | ) 48 | 49 | type EntityAnimation struct { 50 | EntityID int32 51 | Animation byte 52 | } 53 | 54 | func (r EntityAnimation) Encode(writer *minecraft.Writer) { 55 | writer.WriteVarint(0x06) 56 | writer.WriteVarint(r.EntityID) 57 | writer.WriteByte(r.Animation) 58 | } 59 | 60 | type EntityPosition struct { 61 | EntityId int32 62 | DeltaX int16 63 | DeltaY int16 64 | DeltaZ int16 65 | OnGround bool 66 | } 67 | 68 | func (r EntityPosition) Encode(writer *minecraft.Writer) { 69 | writer.WriteVarint(0x29) 70 | writer.WriteVarint(r.EntityId) 71 | writer.WriteShort(r.DeltaX) 72 | writer.WriteShort(r.DeltaY) 73 | writer.WriteShort(r.DeltaZ) 74 | writer.WriteBoolean(r.OnGround) 75 | } 76 | 77 | type EntityPositionAndRotation struct { 78 | EntityId int32 79 | DeltaX int16 80 | DeltaY int16 81 | DeltaZ int16 82 | Yaw minecraft.Angle 83 | Pitch minecraft.Angle 84 | OnGround bool 85 | } 86 | 87 | func (r EntityPositionAndRotation) Encode(writer *minecraft.Writer) { 88 | writer.WriteVarint(0x2A) 89 | writer.WriteVarint(r.EntityId) 90 | writer.WriteShort(r.DeltaX) 91 | writer.WriteShort(r.DeltaY) 92 | writer.WriteShort(r.DeltaZ) 93 | writer.WriteAngle(r.Yaw) 94 | writer.WriteAngle(r.Pitch) 95 | writer.WriteBoolean(r.OnGround) 96 | } 97 | 98 | type EntityRotation struct { 99 | EntityID int32 100 | Yaw minecraft.Angle 101 | Pitch minecraft.Angle 102 | OnGround bool 103 | } 104 | 105 | func (r EntityRotation) Encode(writer *minecraft.Writer) { 106 | writer.WriteVarint(0x2B) 107 | writer.WriteVarint(r.EntityID) 108 | writer.WriteAngle(r.Yaw) 109 | writer.WriteAngle(r.Pitch) 110 | writer.WriteBoolean(r.OnGround) 111 | } 112 | 113 | type EntityMovement struct { 114 | EntityID int32 115 | } 116 | 117 | func (r EntityMovement) Encode(writer *minecraft.Writer) { 118 | writer.WriteVarint(0x2C) 119 | writer.WriteVarint(r.EntityID) 120 | } 121 | 122 | type DestroyEntities struct { 123 | EntityIDs []int32 124 | } 125 | 126 | func (r DestroyEntities) Encode(writer *minecraft.Writer) { 127 | writer.WriteVarint(0x38) 128 | writer.WriteVarint(int32(len(r.EntityIDs))) 129 | for _, eid := range r.EntityIDs { 130 | writer.WriteVarint(eid) 131 | } 132 | } 133 | 134 | // TODO: Remove Entity Effect 135 | 136 | type EntityHeadLook struct { 137 | EntityID int32 138 | HeadYaw minecraft.Angle 139 | } 140 | 141 | func (r EntityHeadLook) Encode(writer *minecraft.Writer) { 142 | writer.WriteVarint(0x3C) 143 | writer.WriteVarint(r.EntityID) 144 | writer.WriteAngle(r.HeadYaw) 145 | } 146 | 147 | type IMetadata interface { 148 | WriteMetadata(writer *minecraft.EntityMetadataWriter) 149 | } 150 | 151 | type EntityMetadata struct { 152 | EntityID int32 153 | Metadata IMetadata 154 | } 155 | 156 | func (r EntityMetadata) Encode(writer *minecraft.Writer) { 157 | writer.WriteVarint(0x44) 158 | writer.WriteVarint(r.EntityID) 159 | ew := writer.StartEntityMetadata() 160 | r.Metadata.WriteMetadata(ew) 161 | ew.Done() 162 | } 163 | 164 | // TODO: Attach Entity 165 | 166 | // TODO: Entity Velocity 167 | 168 | const ( 169 | EquipmentMainHand = int32(iota) 170 | EquipmentOffHand 171 | EquipmentBoots 172 | EquipmentLeggings 173 | EquipmentChestplate 174 | EquipmentHelmet 175 | ) 176 | 177 | type EntityEquipment struct { 178 | EntityID int32 179 | Slot int32 180 | Item *Slot 181 | } 182 | 183 | func (r EntityEquipment) Encode(writer *minecraft.Writer) { 184 | writer.WriteVarint(0x47) 185 | writer.WriteVarint(r.EntityID) 186 | writer.WriteVarint(r.Slot) 187 | r.Item.Encode(writer) 188 | } 189 | 190 | // TODO: Entity Sound Effect 191 | 192 | type EntityTeleport struct { 193 | EntityID int32 194 | X float64 195 | Y float64 196 | Z float64 197 | Yaw minecraft.Angle 198 | Pitch minecraft.Angle 199 | OnGround bool 200 | } 201 | 202 | func (r EntityTeleport) Encode(writer *minecraft.Writer) { 203 | writer.WriteVarint(0x57) 204 | writer.WriteVarint(r.EntityID) 205 | writer.WriteDouble(r.X) 206 | writer.WriteDouble(r.Y) 207 | writer.WriteDouble(r.Z) 208 | writer.WriteAngle(r.Yaw) 209 | writer.WriteAngle(r.Pitch) 210 | writer.WriteBoolean(r.OnGround) 211 | } 212 | 213 | // TODO: Entity Properties 214 | 215 | // TODO: Entity Effect -------------------------------------------------------------------------------- /minecraft/proto/play/menu.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "fmt" 5 | "github.com/itay2805/mcserver/minecraft" 6 | "github.com/itay2805/mcserver/minecraft/item" 7 | ) 8 | 9 | type Slot struct { 10 | ItemID int32 11 | ItemCount byte 12 | NBT interface{} 13 | } 14 | 15 | func (s *Slot) String() string { 16 | if s == nil { 17 | return fmt.Sprintf("Slot{}") 18 | } else { 19 | return fmt.Sprintf("Slot{ Item: %s, Count: %d, NBT: %s }", item.GetById(int(s.ItemID)).Name, s.ItemCount, s.NBT) 20 | } 21 | } 22 | 23 | func (s *Slot) CreateFake() *Slot { 24 | if s == nil { 25 | return nil 26 | } else { 27 | // TODO: make this handle nbt data properly 28 | return &Slot{ 29 | ItemID: s.ItemID, 30 | ItemCount: 69, 31 | NBT: nil, 32 | } 33 | } 34 | } 35 | 36 | func (s *Slot) Encode(writer *minecraft.Writer) { 37 | if s == nil { 38 | writer.WriteBoolean(false) 39 | } else { 40 | writer.WriteBoolean(true) 41 | writer.WriteVarint(s.ItemID) 42 | writer.WriteByte(s.ItemCount) 43 | if s.NBT == nil { 44 | _ = minecraft.NbtMarshal(writer, struct{}{}) 45 | } else { 46 | _ = minecraft.NbtMarshal(writer, s.NBT) 47 | } 48 | } 49 | } 50 | 51 | type SetSlot struct { 52 | WindowID int8 53 | Slot int16 54 | SlotData *Slot 55 | } 56 | 57 | func (s SetSlot) Encode(writer *minecraft.Writer) { 58 | writer.WriteVarint(0x17) 59 | writer.WriteByte(byte(s.WindowID)) 60 | writer.WriteShort(s.Slot) 61 | s.SlotData.Encode(writer) 62 | } 63 | -------------------------------------------------------------------------------- /minecraft/proto/play/other.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import "github.com/itay2805/mcserver/minecraft" 4 | 5 | const ( 6 | // TODO: add from https://wiki.vg/Protocol#Effect 7 | EffectBlockBreak = int32(2001) 8 | ) 9 | 10 | type Effect struct { 11 | EffectID int32 12 | Location minecraft.Position 13 | Data int32 14 | DisableRelativeVolume bool 15 | } 16 | 17 | func (r Effect) Encode(writer *minecraft.Writer) { 18 | writer.WriteVarint(0x23) 19 | writer.WriteInt(r.EffectID) 20 | writer.WritePosition(r.Location) 21 | writer.WriteInt(r.Data) 22 | writer.WriteBoolean(r.DisableRelativeVolume) 23 | } 24 | -------------------------------------------------------------------------------- /minecraft/proto/play/player.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/itay2805/mcserver/minecraft" 6 | ) 7 | 8 | const ( 9 | ChatBox = 0 10 | SystemMessage = 1 11 | GameInfo = 2 12 | ) 13 | 14 | type ChatMessage struct { 15 | Data minecraft.Chat 16 | Position byte 17 | } 18 | 19 | func (r ChatMessage) Encode(writer *minecraft.Writer) { 20 | writer.WriteVarint(0x0F) 21 | writer.WriteChat(r.Data) 22 | writer.WriteByte(r.Position) 23 | } 24 | 25 | type Disconnect struct { 26 | Reason minecraft.Chat 27 | } 28 | 29 | func (r Disconnect) Encode(writer *minecraft.Writer) { 30 | writer.WriteVarint(0x1B) 31 | writer.WriteChat(r.Reason) 32 | } 33 | 34 | type KeepAlive struct { 35 | KeepAliveId int64 36 | } 37 | 38 | func (r KeepAlive) Encode(writer *minecraft.Writer) { 39 | writer.WriteVarint(0x21) 40 | writer.WriteLong(r.KeepAliveId) 41 | } 42 | 43 | type JoinGame struct { 44 | EntityId int32 45 | Gamemode byte 46 | Dimension int32 47 | HashedSeed int64 48 | LevelType string 49 | ViewDistance int32 50 | ReducedDebugInfo bool 51 | EnableRespawnScreen bool 52 | } 53 | 54 | func (r JoinGame) Encode(writer *minecraft.Writer) { 55 | writer.WriteVarint(0x26) 56 | writer.WriteInt(r.EntityId) 57 | writer.WriteByte(r.Gamemode) 58 | writer.WriteInt(r.Dimension) 59 | writer.WriteLong(r.HashedSeed) 60 | // Max Players, Was once used by the client to draw the player list, but now is ignored 61 | // source: https://wiki.vg/Protocol#Join_Game 62 | writer.WriteByte(0) 63 | writer.WriteString(r.LevelType) 64 | writer.WriteVarint(r.ViewDistance) 65 | writer.WriteBoolean(r.ReducedDebugInfo) 66 | writer.WriteBoolean(r.EnableRespawnScreen) 67 | } 68 | 69 | type PlayerInfo struct { 70 | AddPlayer []PIAddPlayer 71 | UpdateGamemode []PIUpdateGamemode 72 | UpdateLatency []PIUpdateLatency 73 | UpdateDisplayName []PIDisplayName 74 | RemovePlayer []PIRemovePlayer 75 | } 76 | 77 | func (p PlayerInfo) Encode(writer *minecraft.Writer) { 78 | writer.WriteVarint(0x34) 79 | 80 | if len(p.AddPlayer) != 0 { 81 | writer.WriteVarint(0) 82 | writer.WriteVarint(int32(len(p.AddPlayer))) 83 | for _, u := range p.AddPlayer { 84 | u.Encode(writer) 85 | } 86 | 87 | } else if len(p.UpdateGamemode) != 0 { 88 | writer.WriteVarint(1) 89 | writer.WriteVarint(int32(len(p.UpdateGamemode))) 90 | for _, u := range p.UpdateGamemode { 91 | u.Encode(writer) 92 | } 93 | 94 | } else if len(p.UpdateLatency) != 0 { 95 | writer.WriteVarint(2) 96 | writer.WriteVarint(int32(len(p.UpdateLatency))) 97 | for _, u := range p.UpdateLatency { 98 | u.Encode(writer) 99 | } 100 | 101 | } else if len(p.UpdateDisplayName) != 0 { 102 | writer.WriteVarint(3) 103 | writer.WriteVarint(int32(len(p.UpdateDisplayName))) 104 | for _, u := range p.UpdateDisplayName { 105 | u.Encode(writer) 106 | } 107 | 108 | } else if len(p.RemovePlayer) != 0 { 109 | writer.WriteVarint(4) 110 | writer.WriteVarint(int32(len(p.RemovePlayer))) 111 | for _, u := range p.RemovePlayer { 112 | u.Encode(writer) 113 | } 114 | 115 | } 116 | } 117 | 118 | type PIAddPlayer struct { 119 | UUID uuid.UUID 120 | Name string 121 | //Props []mojang.PlayerProperty 122 | Gamemode int32 123 | Ping int32 124 | DisplayName *minecraft.Chat 125 | } 126 | 127 | func (p PIAddPlayer) Encode(writer *minecraft.Writer) { 128 | writer.WriteUUID(p.UUID) 129 | writer.WriteString(p.Name) 130 | 131 | writer.WriteVarint(0) 132 | //writer.WriteVarint(int32(len(p.Props))) 133 | //for _, prop := range p.Props { 134 | // writer.WriteString(prop.Name) 135 | // writer.WriteString(prop.Value) 136 | // if len(prop.Signature) != 0 { 137 | // writer.WriteBoolean(true) 138 | // writer.WriteString(prop.Signature) 139 | // } else { 140 | // writer.WriteBoolean(false) 141 | // } 142 | //} 143 | 144 | writer.WriteVarint(p.Gamemode) 145 | writer.WriteVarint(p.Ping) 146 | 147 | if p.DisplayName != nil { 148 | writer.WriteBoolean(true) 149 | writer.WriteChat(*p.DisplayName) 150 | } else { 151 | writer.WriteBoolean(false) 152 | } 153 | } 154 | 155 | type PIUpdateGamemode struct { 156 | UUID uuid.UUID 157 | Gamemode int32 158 | } 159 | 160 | func (p PIUpdateGamemode) Encode(writer *minecraft.Writer) { 161 | writer.WriteUUID(p.UUID) 162 | writer.WriteVarint(p.Gamemode) 163 | } 164 | 165 | type PIUpdateLatency struct { 166 | UUID uuid.UUID 167 | Ping int32 168 | } 169 | 170 | func (p PIUpdateLatency) Encode(writer *minecraft.Writer) { 171 | writer.WriteUUID(p.UUID) 172 | writer.WriteVarint(p.Ping) 173 | } 174 | 175 | type PIDisplayName struct { 176 | UUID uuid.UUID 177 | DisplayName *minecraft.Chat 178 | } 179 | 180 | func (p PIDisplayName) Encode(writer *minecraft.Writer) { 181 | writer.WriteUUID(p.UUID) 182 | if p.DisplayName != nil { 183 | writer.WriteBoolean(true) 184 | writer.WriteChat(*p.DisplayName) 185 | } else { 186 | writer.WriteBoolean(false) 187 | } 188 | } 189 | 190 | type PIRemovePlayer struct { 191 | UUID uuid.UUID 192 | } 193 | 194 | func (p PIRemovePlayer) Encode(writer *minecraft.Writer) { 195 | writer.WriteUUID(p.UUID) 196 | } 197 | 198 | const ( 199 | PlayerPositionXRelative = 0x01 200 | PlayerPositionYRelative = 0x02 201 | PlayerPositionZRelative = 0x04 202 | PlayerPositionYawRelative = 0x08 203 | PlayerPositionPitchRelative = 0x10 204 | ) 205 | 206 | type PlayerPositionAndLook struct { 207 | X, Y, Z float64 208 | Yaw, Pitch float32 209 | Flags byte 210 | TeleportId int32 211 | } 212 | 213 | func (r PlayerPositionAndLook) Encode(writer *minecraft.Writer) { 214 | writer.WriteVarint(0x36) 215 | writer.WriteDouble(r.X) 216 | writer.WriteDouble(r.Y) 217 | writer.WriteDouble(r.Z) 218 | writer.WriteFloat(r.Yaw) 219 | writer.WriteFloat(r.Pitch) 220 | writer.WriteByte(r.Flags) 221 | writer.WriteVarint(r.TeleportId) 222 | } 223 | -------------------------------------------------------------------------------- /minecraft/proto/play/world.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/minecraft" 5 | ) 6 | 7 | type BlockBreakAnimation struct { 8 | EntityId int32 9 | Location minecraft.Position 10 | DestroyStage byte 11 | } 12 | 13 | func (r BlockBreakAnimation) Encode(writer *minecraft.Writer) { 14 | writer.WriteVarint(0x09) 15 | writer.WriteVarint(r.EntityId) 16 | writer.WritePosition(r.Location) 17 | writer.WriteByte(r.DestroyStage) 18 | } 19 | 20 | // TODO: Block Entity Data 21 | 22 | // TODO: Block Action 23 | 24 | type BlockChange struct { 25 | Location minecraft.Position 26 | BlockID uint16 27 | } 28 | 29 | func (r BlockChange) Encode(writer *minecraft.Writer) { 30 | writer.WriteVarint(0x0C) 31 | writer.WritePosition(r.Location) 32 | writer.WriteVarint(int32(r.BlockID)) 33 | } 34 | 35 | type BlockRecord struct { 36 | BlockX byte 37 | BlockZ byte 38 | BlockY byte 39 | BlockState uint16 40 | } 41 | 42 | type MultiBlockChange struct { 43 | ChunkX int32 44 | ChunkZ int32 45 | Records []BlockRecord 46 | } 47 | 48 | func (r MultiBlockChange) Encode(writer *minecraft.Writer) { 49 | writer.WriteVarint(0x10) 50 | writer.WriteInt(r.ChunkX) 51 | writer.WriteInt(r.ChunkZ) 52 | writer.WriteVarint(int32(len(r.Records))) 53 | for _, rec := range r.Records { 54 | writer.WriteByte(rec.BlockX << 4 | rec.BlockZ) 55 | writer.WriteByte(rec.BlockY) 56 | writer.WriteVarint(int32(rec.BlockState)) 57 | } 58 | } 59 | 60 | type UnloadChunk struct { 61 | ChunkX int32 62 | ChunkZ int32 63 | } 64 | 65 | func (p UnloadChunk) Encode(writer *minecraft.Writer) { 66 | writer.WriteVarint(0x1E) 67 | writer.WriteInt(p.ChunkX) 68 | writer.WriteInt(p.ChunkZ) 69 | } 70 | 71 | type UpdateViewPosition struct { 72 | ChunkX int32 73 | ChunkZ int32 74 | } 75 | 76 | func (p UpdateViewPosition) Encode(writer *minecraft.Writer) { 77 | writer.WriteVarint(0x41) 78 | writer.WriteVarint(p.ChunkX) 79 | writer.WriteVarint(p.ChunkZ) 80 | } 81 | 82 | type TimeUpdate struct { 83 | WorldAge int64 84 | TimeOfDay int64 85 | } 86 | 87 | func (p TimeUpdate) Encode(writer *minecraft.Writer) { 88 | writer.WriteVarint(0x4F) 89 | writer.WriteLong(p.WorldAge) 90 | writer.WriteLong(p.TimeOfDay) 91 | } -------------------------------------------------------------------------------- /minecraft/proto/status/packets.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import "github.com/itay2805/mcserver/minecraft" 4 | 5 | type ServerListVersion struct { 6 | Name string `json:"name"` 7 | Protocol int32 `json:"protocol"` 8 | } 9 | 10 | type ServerListPlayerSample struct { 11 | Name string `json:"name"` 12 | Id string `json:"id"` 13 | } 14 | 15 | type ServerListPlayers struct { 16 | Max int `json:"max"` 17 | Online int `json:"online"` 18 | Sample []ServerListPlayerSample `json:"sample"` 19 | } 20 | 21 | type ServerListResponse struct { 22 | Version ServerListVersion `json:"version"` 23 | Players ServerListPlayers `json:"players"` 24 | Description minecraft.Chat `json:"description"` 25 | Favicon string `json:"favicon,omitempty"` 26 | } 27 | 28 | type Response struct { 29 | Response ServerListResponse 30 | } 31 | 32 | func (r Response) Encode(writer *minecraft.Writer) { 33 | writer.WriteVarint(0x00) 34 | writer.WriteJson(r.Response) 35 | } 36 | 37 | type Pong struct { 38 | Payload int64 39 | } 40 | 41 | func (r Pong) Encode(writer *minecraft.Writer) { 42 | writer.WriteVarint(0x01) 43 | writer.WriteLong(r.Payload) 44 | } -------------------------------------------------------------------------------- /minecraft/reader.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "github.com/google/uuid" 7 | "log" 8 | "math" 9 | ) 10 | 11 | type Reader struct { 12 | Data []byte 13 | Offset int 14 | } 15 | 16 | func (reader *Reader) Read(p []byte) (int, error) { 17 | n := copy(p, reader.Data[reader.Offset:]) 18 | reader.Offset += n 19 | return n, nil 20 | } 21 | 22 | func (reader *Reader) ReadBytes(size int) []byte { 23 | if reader.Offset + size > len(reader.Data) { 24 | log.Panicln("Got to end of packet!") 25 | } 26 | Data := reader.Data[reader.Offset:reader.Offset + size] 27 | reader.Offset += size 28 | return Data 29 | } 30 | 31 | func (reader *Reader) ReadBoolean() bool { 32 | b := reader.ReadByte() 33 | if b == 0x01 { 34 | return true 35 | } else if b == 0x00 { 36 | return false 37 | } else { 38 | panic(fmt.Sprint("Invalid boolean value ", b)) 39 | } 40 | } 41 | 42 | func (reader *Reader) ReadByte() byte { 43 | return reader.ReadBytes(1)[0] 44 | } 45 | 46 | func (reader *Reader) ReadShort() int16 { 47 | return int16(binary.BigEndian.Uint16(reader.ReadBytes(2))) 48 | } 49 | 50 | func (reader *Reader) ReadUShort() uint16 { 51 | return binary.BigEndian.Uint16(reader.ReadBytes(2)) 52 | } 53 | 54 | func (reader *Reader) ReadInt() int32 { 55 | return int32(binary.BigEndian.Uint32(reader.ReadBytes(4))) 56 | } 57 | 58 | func (reader *Reader) ReadUInt() uint32 { 59 | return binary.BigEndian.Uint32(reader.ReadBytes(4)) 60 | } 61 | 62 | func (reader *Reader) ReadLong() int64 { 63 | return int64(binary.BigEndian.Uint64(reader.ReadBytes(8))) 64 | } 65 | 66 | func (reader *Reader) ReadULong() uint64 { 67 | return binary.BigEndian.Uint64(reader.ReadBytes(8)) 68 | } 69 | 70 | func (reader *Reader) ReadFloat() float32 { 71 | return math.Float32frombits(binary.BigEndian.Uint32(reader.ReadBytes(4))) 72 | } 73 | 74 | func (reader *Reader) ReadDouble() float64 { 75 | return math.Float64frombits(binary.BigEndian.Uint64(reader.ReadBytes(8))) 76 | } 77 | 78 | func (reader *Reader) ReadString(maxLen int) string { 79 | l := int(reader.ReadVarint()) 80 | if l > maxLen { 81 | log.Panicln("String is too large!") 82 | } 83 | return string(reader.ReadBytes(l)) 84 | } 85 | 86 | func (reader *Reader) ReadChat() Chat { 87 | l := int(reader.ReadVarint()) 88 | if l > 32767 { 89 | log.Panicln("String is too large!") 90 | } 91 | return NewChat(reader.ReadBytes(32767)) 92 | } 93 | 94 | func (reader *Reader) ReadIdentifier() string { 95 | return reader.ReadString(32767) 96 | } 97 | 98 | func (reader *Reader) ReadVarint() int32 { 99 | numRead := 0 100 | result := int32(0) 101 | for { 102 | read := reader.ReadByte() 103 | value := read & 0b01111111 104 | result |= int32(value) << (7 * numRead) 105 | 106 | numRead++ 107 | if numRead > 5 { 108 | log.Panicln("Varint is too big") 109 | } 110 | 111 | if (read & 0b10000000) == 0 { 112 | return result 113 | } 114 | } 115 | } 116 | 117 | func (reader *Reader) ReadVarlong() int64 { 118 | numRead := 0 119 | result := int64(0) 120 | for { 121 | read := reader.ReadByte() 122 | value := read & 0b01111111 123 | result |= int64(value) << (7 * numRead) 124 | 125 | numRead++ 126 | if numRead > 10 { 127 | log.Panicln("Varlong is too big") 128 | } 129 | 130 | if (read & 0b10000000) == 0 { 131 | return result 132 | } 133 | } 134 | } 135 | 136 | // TODO: entity metaData 137 | 138 | // TODO: Slot 139 | 140 | // TODO: NBT Tag 141 | 142 | func (reader *Reader) ReadPosition() Position { 143 | return ParsePosition(reader.ReadULong()) 144 | } 145 | 146 | func (reader *Reader) ReadAngle() Angle { 147 | return Angle(reader.ReadByte()) 148 | } 149 | 150 | func (reader *Reader) ReadUUID() uuid.UUID { 151 | u, _ := uuid.FromBytes(reader.ReadBytes(16)) 152 | return u 153 | } 154 | 155 | func (reader *Reader) ReadUUIDFromString() uuid.UUID { 156 | return uuid.MustParse(reader.ReadString(36)) 157 | } 158 | -------------------------------------------------------------------------------- /minecraft/types.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "fmt" 5 | math2 "github.com/itay2805/mcserver/math" 6 | "log" 7 | "math" 8 | ) 9 | 10 | type Position struct { 11 | X, Y, Z int 12 | } 13 | 14 | func (p Position) String() string { 15 | return fmt.Sprintf("Position{X: %d, Y: %d, Z: %d}", p.X, p.Y, p.Z) 16 | } 17 | 18 | func (p Position) ToPoint() math2.Point { 19 | return math2.NewPoint(float64(p.X), float64(p.Y), float64(p.Z)) 20 | } 21 | 22 | func (p Position) ApplyFace(face Face) Position { 23 | switch face { 24 | case FaceBottom: 25 | return Position{ 26 | X: p.X, 27 | Y: p.Y - 1, 28 | Z: p.Z, 29 | } 30 | case FaceTop: 31 | return Position{ 32 | X: p.X, 33 | Y: p.Y + 1, 34 | Z: p.Z, 35 | } 36 | case FaceNorth: 37 | return Position{ 38 | X: p.X, 39 | Y: p.Y, 40 | Z: p.Z - 1, 41 | } 42 | case FaceSouth: 43 | return Position{ 44 | X: p.X, 45 | Y: p.Y, 46 | Z: p.Z + 1, 47 | } 48 | case FaceWest: 49 | return Position{ 50 | X: p.X - 1, 51 | Y: p.Y, 52 | Z: p.Z, 53 | } 54 | case FaceEast: 55 | return Position{ 56 | X: p.X + 1, 57 | Y: p.Y, 58 | Z: p.Z, 59 | } 60 | } 61 | log.Panicln("Invalid face", face) 62 | return Position{} 63 | } 64 | 65 | func ParsePosition(val uint64) Position { 66 | p := Position{ 67 | X: int((val >> 38) & 0x3FFFFFF), 68 | Y: int(val & 0xFFF), 69 | Z: int((val >> 12) & 0x3FFFFFF), 70 | } 71 | 72 | if p.X > 33554432 { 73 | p.X -= 67108864 74 | } 75 | 76 | if p.Z > 33554432 { 77 | p.Z -= 67108864 78 | } 79 | 80 | if p.Y > 2048 { 81 | p.Y -= 4096 82 | } 83 | 84 | return p 85 | } 86 | 87 | func (p Position) Pack() uint64 { 88 | return uint64(((p.X & 0x3FFFFFF) << 38) | ((p.Z & 0x3FFFFFF) << 12) | (p.Y & 0xFFF)) 89 | } 90 | 91 | type Angle uint8 92 | 93 | func ToAngle(v float32) Angle { 94 | return Angle(v * 256.0 / 360.0) 95 | } 96 | 97 | func (a Angle) ToRadians() float64 { 98 | return float64(a) * math.Pi / 128.0 99 | } 100 | 101 | type Face int 102 | 103 | const ( 104 | FaceBottom = Face(iota) 105 | FaceTop 106 | FaceNorth 107 | FaceSouth 108 | FaceWest 109 | FaceEast 110 | ) 111 | 112 | func (f Face) Invert() Face { 113 | switch f { 114 | case FaceBottom: return FaceTop 115 | case FaceTop: return FaceBottom 116 | case FaceNorth: return FaceSouth 117 | case FaceSouth: return FaceNorth 118 | case FaceWest: return FaceEast 119 | case FaceEast: return FaceWest 120 | default: 121 | return FaceEast 122 | } 123 | } 124 | 125 | func (f Face) String() string { 126 | switch f { 127 | case FaceBottom: return "FaceBottom" 128 | case FaceTop: return "FaceTop" 129 | case FaceNorth: return "FaceNorth" 130 | case FaceSouth: return "FaceSouth" 131 | case FaceWest: return "FaceWest" 132 | case FaceEast: return "FaceEast" 133 | default: 134 | return fmt.Sprintf("Face(%d)", f) 135 | } 136 | } 137 | 138 | 139 | type Shape int 140 | 141 | const ( 142 | ShapeStraight = Shape(iota) 143 | ShapeInnerLeft 144 | ShapeInnerRight 145 | ShapeOuterLeft 146 | ShapeOuterRight 147 | ) 148 | 149 | func (f Shape) String() string { 150 | switch f { 151 | case ShapeStraight: return "ShapeStraight" 152 | case ShapeInnerLeft: return "ShapeInnerLeft" 153 | case ShapeInnerRight: return "ShapeInnerRight" 154 | case ShapeOuterLeft: return "ShapeOuterLeft" 155 | case ShapeOuterRight: return "ShapeOuterRight" 156 | default: 157 | return fmt.Sprintf("Shape(%d)", f) 158 | } 159 | } 160 | 161 | 162 | type Hinge int 163 | 164 | const ( 165 | HingeLeft = Hinge(iota) 166 | HingeRight 167 | ) 168 | 169 | func (f Hinge) String() string { 170 | switch f { 171 | case HingeLeft: return "HingeLeft" 172 | case HingeRight: return "HingeRight" 173 | default: 174 | return fmt.Sprintf("Hinge(%d)", f) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /minecraft/world/generator/flatgrass/generator.go: -------------------------------------------------------------------------------- 1 | package flatgrass 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/minecraft" 5 | "github.com/itay2805/mcserver/minecraft/block" 6 | "github.com/itay2805/mcserver/minecraft/chunk" 7 | ) 8 | 9 | type FlatgraassGenerator struct { 10 | stone *block.Block 11 | grass *block.Block 12 | dirt *block.Block 13 | } 14 | 15 | func (d *FlatgraassGenerator) GetSpawn() minecraft.Position { 16 | return minecraft.Position{ X: 8, Y: 62, Z: 8 } 17 | } 18 | 19 | func (d *FlatgraassGenerator) GenerateChunk(x, z int) *chunk.Chunk { 20 | c := chunk.NewChunk(x, z) 21 | 22 | for x := 0; x < 16; x++ { 23 | for z := 0; z < 16; z++ { 24 | c.SetBlockState(x, 0, z, block.Bedrock.DefaultStateId) 25 | 26 | for y := 1; y < 48; y++ { 27 | c.SetBlockState(x, y, z, block.Stone.DefaultStateId) 28 | } 29 | 30 | for y := 48; y < 61; y++ { 31 | c.SetBlockState(x, y, z, block.Dirt.DefaultStateId) 32 | } 33 | 34 | c.SetBlockState(x, 61, z, block.GrassBlock.DefaultStateId) 35 | } 36 | } 37 | 38 | return c 39 | } -------------------------------------------------------------------------------- /minecraft/world/lighting.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "github.com/eapache/queue" 5 | "github.com/itay2805/mcserver/minecraft/block" 6 | "github.com/itay2805/mcserver/minecraft/chunk" 7 | ) 8 | 9 | // the light updates that need to be processed 10 | var blockLightUpdates = queue.New() 11 | var skyLightUpdates = queue.New() 12 | 13 | type lightUpdate struct { 14 | x, y, z int 15 | chunk *chunk.Chunk 16 | world *World 17 | } 18 | 19 | // queue a light update to happen later on 20 | func QueueLightUpdate(w *World, c *chunk.Chunk, x, y, z int) { 21 | blockLightUpdates.Add(lightUpdate{ 22 | x: x, 23 | y: y, 24 | z: z, 25 | chunk: c, 26 | world: w, 27 | }) 28 | } 29 | 30 | // process a light update 31 | func ProcessLightUpdates() { 32 | for blockLightUpdates.Length() > 0 { 33 | update := blockLightUpdates.Remove().(lightUpdate) 34 | updateBlockLight(blockLightUpdates, update.world, update.chunk, update.x, update.y, update.z) 35 | } 36 | 37 | for skyLightUpdates.Length() > 0 { 38 | update := skyLightUpdates.Remove().(lightUpdate) 39 | updateSkylight(skyLightUpdates, update.world, update.chunk, update.x, update.y, update.z) 40 | } 41 | } 42 | 43 | // light up a single chunk, used when generating new chunks 44 | func (w *World) lightChunk(c *chunk.Chunk) { 45 | blockLightUpdates := queue.New() 46 | skyLightUpdates := queue.New() 47 | 48 | for x := 0; x < 16; x++ { 49 | for z := 0; z < 16; z++ { 50 | c.SetSkyLight(x, 255, z, 15) 51 | 52 | currLight := 15 53 | for y := 254; y >= 0; y-- { 54 | typ := block.GetByStateId(c.GetBlockState(x, y, z)) 55 | 56 | currLight -= typ.FilterLight 57 | if currLight < 0 { 58 | currLight = 0 59 | } 60 | 61 | if !typ.Transparent { 62 | if typ.EmitLight > 0 { 63 | blockLightUpdates.Add(lightUpdate{ 64 | x: x, 65 | y: y, 66 | z: z, 67 | chunk: c, 68 | world: w, 69 | }) 70 | } 71 | } else if currLight == 0 { 72 | skyLightUpdates.Add(lightUpdate{ 73 | x: x, 74 | y: y, 75 | z: z, 76 | chunk: c, 77 | world: w, 78 | }) 79 | } 80 | 81 | c.SetSkyLight(x, y, z, currLight) 82 | } 83 | } 84 | } 85 | 86 | for blockLightUpdates.Length() > 0 { 87 | update := blockLightUpdates.Remove().(lightUpdate) 88 | updateBlockLight(blockLightUpdates, update.world, update.chunk, update.x, update.y, update.z) 89 | } 90 | 91 | for skyLightUpdates.Length() > 0 { 92 | update := skyLightUpdates.Remove().(lightUpdate) 93 | updateSkylight(skyLightUpdates, update.world, update.chunk, update.x, update.y, update.z) 94 | } 95 | } 96 | 97 | // return the max of all nums 98 | func max(nums ...int) int { 99 | largest := nums[0] 100 | for _, num := range nums { 101 | if num > largest { 102 | largest = num 103 | } 104 | } 105 | return largest 106 | } 107 | 108 | // This function will do a sky light update from the given block in 109 | // a flood fill manner 110 | func updateSkylight(q *queue.Queue, w *World, c *chunk.Chunk, x, y, z int) { 111 | typ := block.GetByStateId(c.GetBlockState(x, y, z)) 112 | light := 0 113 | 114 | if typ.FilterLight == 15 { 115 | light = 0 116 | } else { 117 | var sle int 118 | var slw int 119 | var slu int 120 | var sld int 121 | var sls int 122 | var sln int 123 | 124 | if x < 15 { 125 | sle = c.GetSkyLight(x + 1, y, z) 126 | } else { 127 | eastC := w.GetChunk(c.X + 1, c.Z) 128 | if eastC == nil { 129 | sle = 0 130 | } else { 131 | sle = eastC.GetSkyLight(0, y, z) 132 | } 133 | } 134 | 135 | if x > 0 { 136 | slw = c.GetSkyLight(x - 1, y, z) 137 | } else { 138 | westC := w.GetChunk(c.X - 1, c.Z) 139 | if westC == nil { 140 | slw = 0 141 | } else { 142 | slw = westC.GetSkyLight(15, y, z) 143 | } 144 | } 145 | 146 | if y < 255 { 147 | slu = c.GetSkyLight(x, y + 1, z) 148 | } else { 149 | slu = 15 150 | } 151 | 152 | if y > 0 { 153 | sld = c.GetSkyLight(x, y - 1, z) 154 | } else { 155 | sld = 0 156 | } 157 | 158 | if z < 15 { 159 | sls = c.GetSkyLight(x, y, z + 1) 160 | } else { 161 | southC := w.GetChunk(c.X, c.Z + 1) 162 | if southC == nil { 163 | sls = 0 164 | } else { 165 | sls = southC.GetSkyLight(x, y, 0) 166 | } 167 | } 168 | 169 | if z > 0 { 170 | sln = c.GetSkyLight(x, y, z - 1) 171 | } else { 172 | northC := w.GetChunk(c.X, c.Z - 1) 173 | if northC == nil { 174 | sln = 0 175 | } else { 176 | sln = northC.GetSkyLight(x, y, 15) 177 | } 178 | } 179 | 180 | brightest := max(sle, slw, slu, sld, sls, sln, 0) 181 | light = brightest - typ.FilterLight - 1 182 | if light < 0 { 183 | light = 0 184 | } 185 | } 186 | 187 | if c.GetSkyLight(x, y, z) != light { 188 | c.SetSkyLight(x, y, z, light) 189 | 190 | if x < 15 { 191 | q.Add(lightUpdate{ 192 | x: x + 1, 193 | y: y, 194 | z: z, 195 | chunk: c, 196 | world: w, 197 | }) 198 | } 199 | 200 | if x > 0 { 201 | q.Add(lightUpdate{ 202 | x: x - 1, 203 | y: y, 204 | z: z, 205 | chunk: c, 206 | world: w, 207 | }) 208 | } 209 | 210 | if y < 255 { 211 | q.Add(lightUpdate{ 212 | x: x, 213 | y: y + 1, 214 | z: z, 215 | chunk: c, 216 | world: w, 217 | }) 218 | } 219 | 220 | if y > 0 { 221 | q.Add(lightUpdate{ 222 | x: x, 223 | y: y - 1, 224 | z: z, 225 | chunk: c, 226 | world: w, 227 | }) 228 | 229 | } 230 | 231 | if z < 15 { 232 | q.Add(lightUpdate{ 233 | x: x, 234 | y: y, 235 | z: z + 1, 236 | chunk: c, 237 | world: w, 238 | }) 239 | 240 | } 241 | 242 | if z > 0 { 243 | q.Add(lightUpdate{ 244 | x: x, 245 | y: y, 246 | z: z - 1, 247 | chunk: c, 248 | world: w, 249 | }) 250 | } 251 | } 252 | } 253 | 254 | func updateBlockLight(q *queue.Queue, w *World, c *chunk.Chunk, x, y, z int) { 255 | typ := block.GetByStateId(c.GetBlockState(x, y, z)) 256 | 257 | light := 0 258 | 259 | if typ.FilterLight != 0 { 260 | light = typ.EmitLight 261 | } else { 262 | var ble int 263 | var blw int 264 | var blu int 265 | var bld int 266 | var bls int 267 | var bln int 268 | 269 | if x < 15 { 270 | ble = c.GetBlockLight(x + 1, y, z) 271 | } else { 272 | eastC := w.GetChunk(c.X + 1, c.Z) 273 | if eastC == nil { 274 | ble = 0 275 | } else { 276 | ble = eastC.GetBlockLight(0, y, z) 277 | } 278 | } 279 | 280 | if x > 0 { 281 | blw = c.GetBlockLight(x - 1, y, z) 282 | } else { 283 | westC := w.GetChunk(c.X - 1, c.Z) 284 | if westC == nil { 285 | blw = 0 286 | } else { 287 | blw = westC.GetBlockLight(15, y, z) 288 | } 289 | } 290 | 291 | if y < 255 { 292 | blu = c.GetBlockLight(x, y + 1, z) 293 | } else { 294 | blu = 15 295 | } 296 | 297 | if y > 0 { 298 | bld = c.GetBlockLight(x, y - 1, z) 299 | } else { 300 | bld = 0 301 | } 302 | 303 | if z < 15 { 304 | bls = c.GetBlockLight(x, y, z + 1) 305 | } else { 306 | southC := w.GetChunk(c.X, c.Z + 1) 307 | if southC == nil { 308 | bls = 0 309 | } else { 310 | bls = southC.GetBlockLight(x, y, 0) 311 | } 312 | } 313 | 314 | if z > 0 { 315 | bln = c.GetBlockLight(x, y, z - 1) 316 | } else { 317 | northC := w.GetChunk(c.X, c.Z - 1) 318 | if northC == nil { 319 | bln = 0 320 | } else { 321 | bln = northC.GetBlockLight(x, y, 15) 322 | } 323 | } 324 | 325 | brightest := max(ble, blw, blu, bld, bls, bln, 0) 326 | light = brightest - 1 + typ.EmitLight 327 | if light < 0 { 328 | light = 0 329 | } else if light > 15 { 330 | light = 15 331 | } 332 | } 333 | 334 | if c.GetBlockLight(x, y, z) != light { 335 | c.SetBlockLight(x, y, z, light) 336 | 337 | if x < 15 { 338 | q.Add(lightUpdate{ 339 | x: x + 1, 340 | y: y, 341 | z: z, 342 | chunk: c, 343 | world: w, 344 | }) 345 | } 346 | 347 | if x > 0 { 348 | q.Add(lightUpdate{ 349 | x: x - 1, 350 | y: y, 351 | z: z, 352 | chunk: c, 353 | world: w, 354 | }) 355 | } 356 | 357 | if y < 255 { 358 | q.Add(lightUpdate{ 359 | x: x, 360 | y: y + 1, 361 | z: z, 362 | chunk: c, 363 | world: w, 364 | }) 365 | } 366 | 367 | if y > 0 { 368 | q.Add(lightUpdate{ 369 | x: x, 370 | y: y - 1, 371 | z: z, 372 | chunk: c, 373 | world: w, 374 | }) 375 | 376 | } 377 | 378 | if z < 15 { 379 | q.Add(lightUpdate{ 380 | x: x, 381 | y: y, 382 | z: z + 1, 383 | chunk: c, 384 | world: w, 385 | }) 386 | 387 | } 388 | 389 | if z > 0 { 390 | q.Add(lightUpdate{ 391 | x: x, 392 | y: y, 393 | z: z - 1, 394 | chunk: c, 395 | world: w, 396 | }) 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /minecraft/world/provider/nullprovider/provider.go: -------------------------------------------------------------------------------- 1 | package nullprovider 2 | 3 | import "github.com/itay2805/mcserver/minecraft/chunk" 4 | 5 | type NullProvider struct { 6 | } 7 | 8 | func (n *NullProvider) SaveChunk(chunk *chunk.Chunk) { 9 | 10 | } 11 | 12 | func (n *NullProvider) LoadChunk(x, z int) *chunk.Chunk { 13 | return nil 14 | } 15 | 16 | -------------------------------------------------------------------------------- /minecraft/world/world.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/minecraft" 5 | "github.com/itay2805/mcserver/minecraft/chunk" 6 | "sync" 7 | ) 8 | 9 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 10 | // World generation 11 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 12 | 13 | type Generator interface { 14 | // 15 | // Return the world's spawn location 16 | // 17 | GetSpawn() minecraft.Position 18 | 19 | // 20 | // Generate a new chunk at the given 21 | // coordinates 22 | // 23 | GenerateChunk(x, z int) *chunk.Chunk 24 | } 25 | 26 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | // World providers 28 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 29 | 30 | type Provider interface { 31 | // 32 | // Save the chunk to persistent storage 33 | // 34 | SaveChunk(chunk *chunk.Chunk) 35 | 36 | // 37 | // Load the chunk from persistent storage 38 | // if does not exists then will return nil 39 | // 40 | LoadChunk(x, z int) *chunk.Chunk 41 | } 42 | 43 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 44 | // The world itself 45 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 46 | 47 | type ChunkPos struct { 48 | X, Z int 49 | } 50 | 51 | type World struct { 52 | Provider Provider 53 | Generator Generator 54 | chunks sync.Map 55 | } 56 | 57 | func NewWorld(provider Provider, generator Generator) *World { 58 | return &World{ 59 | Provider: provider, 60 | Generator: generator, 61 | chunks: sync.Map{}, 62 | } 63 | } 64 | 65 | // get a chunk that is already loaded in memory 66 | func (w *World) GetChunk(x, z int) *chunk.Chunk { 67 | if c, ok := w.chunks.Load(ChunkPos{ x, z }); ok { 68 | return c.(*chunk.Chunk) 69 | } 70 | return nil 71 | } 72 | 73 | // will load a new channel if not loaded already or generate 74 | // a new one if does not exist at all 75 | func (w *World) LoadChunk(x, z int) *chunk.Chunk { 76 | // try to get from memory 77 | c := w.GetChunk(x, z) 78 | if c != nil { 79 | return c 80 | } 81 | 82 | // try to load from file 83 | c = w.Provider.LoadChunk(x, z) 84 | if c != nil { 85 | w.chunks.Store(ChunkPos{ x, z }, c) 86 | return c 87 | } 88 | 89 | // generate a new one 90 | c = w.Generator.GenerateChunk(x, z) 91 | w.lightChunk(c) 92 | w.chunks.Store(ChunkPos{ x, z }, c) 93 | return c 94 | } 95 | 96 | func (w *World) GetBlockState(x, y, z int) uint16 { 97 | chunkX := x >> 4 98 | chunkZ := z >> 4 99 | c := w.LoadChunk(chunkX, chunkZ) 100 | return c.GetBlockState(x & 0xf, y, z & 0xf) 101 | } 102 | 103 | func (w *World) SetBlockState(x, y, z int, state uint16) { 104 | chunkX := x >> 4 105 | chunkZ := z >> 4 106 | c := w.LoadChunk(chunkX, chunkZ) 107 | c.SetBlockState(x & 0xf, y, z & 0xf, state) 108 | } 109 | 110 | func (w *World) GetSkyLight(x, y, z int) int { 111 | chunkX := x >> 4 112 | chunkZ := z >> 4 113 | c := w.LoadChunk(chunkX, chunkZ) 114 | return c.GetSkyLight(x & 0xf, y, z & 0xf) 115 | } 116 | 117 | func (w *World) SetSkyLight(x, y, z int, light int) { 118 | chunkX := x >> 4 119 | chunkZ := z >> 4 120 | c := w.LoadChunk(chunkX, chunkZ) 121 | c.SetSkyLight(x & 0xf, y, z & 0xf, light) 122 | } 123 | 124 | func (w *World) GetBlockLight(x, y, z int) int { 125 | chunkX := x >> 4 126 | chunkZ := z >> 4 127 | c := w.LoadChunk(chunkX, chunkZ) 128 | return c.GetBlockLight(x & 0xf, y, z & 0xf) 129 | } 130 | 131 | func (w *World) SetBlockLight(x, y, z int, light int) { 132 | chunkX := x >> 4 133 | chunkZ := z >> 4 134 | c := w.LoadChunk(chunkX, chunkZ) 135 | c.SetBlockLight(x & 0xf, y, z & 0xf, light) 136 | } 137 | -------------------------------------------------------------------------------- /minecraft/writer.go: -------------------------------------------------------------------------------- 1 | package minecraft 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "github.com/google/uuid" 8 | "log" 9 | ) 10 | 11 | type Writer struct { 12 | bytes.Buffer 13 | } 14 | 15 | func (writer *Writer) Bytes() []byte { 16 | return writer.Buffer.Bytes()[:writer.Buffer.Len()] 17 | } 18 | 19 | func (writer *Writer) WriteBytes(data []byte) { 20 | writer.Buffer.Write(data) 21 | } 22 | 23 | func (writer *Writer) WriteBoolean(val bool) { 24 | if val { 25 | writer.WriteByte(0x01) 26 | } else { 27 | writer.WriteByte(0x00) 28 | } 29 | } 30 | 31 | func (writer *Writer) WriteByte(val byte) { 32 | writer.Buffer.WriteByte(val) 33 | } 34 | 35 | func (writer *Writer) WriteShort(val int16) { 36 | writer.Grow(2) 37 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 38 | } 39 | 40 | func (writer *Writer) WriteUShort(val uint16) { 41 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 42 | } 43 | 44 | func (writer *Writer) WriteInt(val int32) { 45 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 46 | } 47 | 48 | func (writer *Writer) WriteUInt(val uint32) { 49 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 50 | } 51 | 52 | func (writer *Writer) WriteLong(val int64) { 53 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 54 | } 55 | 56 | func (writer *Writer) WriteULong(val uint64) { 57 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 58 | } 59 | 60 | func (writer *Writer) WriteFloat(val float32) { 61 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 62 | } 63 | 64 | func (writer *Writer) WriteDouble(val float64) { 65 | _ = binary.Write(&writer.Buffer, binary.BigEndian, val) 66 | } 67 | 68 | func (writer *Writer) WriteString(val string) { 69 | writer.WriteVarint(int32(len(val))) 70 | writer.Buffer.WriteString(val) 71 | } 72 | 73 | func (writer *Writer) WriteChat(val Chat) { 74 | b := val.ToJSON() 75 | writer.WriteVarint(int32(len(b))) 76 | writer.WriteBytes(b) 77 | } 78 | 79 | func (writer *Writer) WriteJson(val interface{}) { 80 | b, err := json.Marshal(val) 81 | if err != nil { 82 | log.Panicln(err) 83 | } 84 | writer.WriteVarint(int32(len(b))) 85 | writer.WriteBytes(b) 86 | } 87 | 88 | // TODO: writeIdentifier 89 | 90 | func (writer *Writer) WriteVarint(val int32) { 91 | raw := uint32(val) 92 | for { 93 | temp := byte(raw & 0b01111111) 94 | raw >>= 7 95 | if raw != 0 { 96 | temp |= 0b10000000 97 | } 98 | writer.WriteByte(temp) 99 | if raw == 0 { 100 | break 101 | } 102 | } 103 | } 104 | 105 | func (writer *Writer) WriteVarlong(val int64) { 106 | raw := uint64(val) 107 | for { 108 | temp := byte(raw & 0b01111111) 109 | raw >>= 7 110 | if raw != 0 { 111 | temp |= 0b10000000 112 | } 113 | writer.WriteByte(temp) 114 | if raw == 0 { 115 | break 116 | } 117 | } 118 | } 119 | 120 | // TODO: entity metaData 121 | 122 | // TODO: SLot 123 | 124 | func (writer *Writer) WritePosition(pos Position) { 125 | writer.WriteULong(pos.Pack()) 126 | } 127 | 128 | func (writer *Writer) WriteAngle(angle Angle) { 129 | writer.WriteByte(byte(angle)) 130 | } 131 | 132 | func (writer *Writer) WriteUUID(val uuid.UUID) { 133 | b, _ := val.MarshalBinary() 134 | writer.WriteBytes(b) 135 | } 136 | 137 | func (writer *Writer) WriteUUIDAsString(val uuid.UUID) { 138 | b, _ := val.MarshalText() 139 | writer.WriteVarint(int32(len(b))) 140 | writer.WriteBytes(b) 141 | } 142 | 143 | func (writer *Writer) StartNBT() NbtWriter { 144 | return NbtWriter{ 145 | w: writer, 146 | hierarchy: []uint8{}, 147 | listSizeStack: []int{}, 148 | listSizeOffsetStack: []int{}, 149 | } 150 | } 151 | 152 | func (writer *Writer) StartEntityMetadata() *EntityMetadataWriter { 153 | return &EntityMetadataWriter{ 154 | writer, 155 | } 156 | } 157 | 158 | type EntityMetadataWriter struct { 159 | w *Writer 160 | } 161 | 162 | type Pose int32 163 | 164 | const ( 165 | PoseStanding = Pose(0) 166 | PoseFallFlying = Pose(1) 167 | PoseSleeping = Pose(2) 168 | PoseSwimming = Pose(3) 169 | PoseSpinAttack = Pose(4) 170 | PoseSneaking = Pose(5) 171 | PoseDying = Pose(6) 172 | ) 173 | 174 | func (writer *EntityMetadataWriter) WriteByte(index byte, val byte) { 175 | writer.w.WriteByte(index) 176 | writer.w.WriteVarint(0) 177 | writer.w.WriteByte(val) 178 | } 179 | 180 | func (writer *EntityMetadataWriter) WriteVarint(index byte, val int32) { 181 | writer.w.WriteByte(index) 182 | writer.w.WriteVarint(1) 183 | writer.w.WriteVarint(val) 184 | } 185 | func (writer *EntityMetadataWriter) WriteFloat(index byte, val float32) { 186 | writer.w.WriteByte(index) 187 | writer.w.WriteVarint(2) 188 | writer.w.WriteFloat(val) 189 | } 190 | func (writer *EntityMetadataWriter) WriteString(index byte, val string) { 191 | writer.w.WriteByte(index) 192 | writer.w.WriteVarint(3) 193 | writer.w.WriteString(val) 194 | } 195 | func (writer *EntityMetadataWriter) WriteBoolean(index byte, val bool) { 196 | writer.w.WriteByte(index) 197 | writer.w.WriteVarint(6) 198 | writer.w.WriteBoolean(val) 199 | } 200 | 201 | func (writer *EntityMetadataWriter) WritePosition(index byte, val Position) { 202 | writer.w.WriteByte(index) 203 | writer.w.WriteVarint(8) 204 | writer.w.WritePosition(val) 205 | } 206 | 207 | func (writer *EntityMetadataWriter) StartNBT(index byte) NbtWriter { 208 | writer.w.WriteByte(index) 209 | writer.w.WriteVarint(13) 210 | return NbtWriter{ 211 | w: writer.w, 212 | hierarchy: []uint8{}, 213 | listSizeStack: []int{}, 214 | listSizeOffsetStack: []int{}, 215 | } 216 | } 217 | 218 | func (writer *EntityMetadataWriter) WritePose(index byte, val Pose) { 219 | writer.w.WriteByte(index) 220 | writer.w.WriteVarint(18) 221 | writer.w.WriteVarint(int32(val)) 222 | } 223 | 224 | func (writer *EntityMetadataWriter) Done() { 225 | writer.w.WriteByte(0xFF) 226 | } 227 | -------------------------------------------------------------------------------- /scripts/generate_biomes.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from datetime import datetime 3 | import os.path as path 4 | import urllib.request 5 | import json 6 | 7 | # TODO: switch to using go:generate 8 | 9 | # This has all of the data we need 10 | import stringcase as stringcase 11 | 12 | with urllib.request.urlopen("https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.15.2/biomes.json") as f: 13 | biomes = json.load(f) 14 | 15 | 16 | def figure_biome_type(name): 17 | # Patterns 18 | if 'ocean' in name: 19 | return 'TypeOcean' 20 | elif 'plains' in name: 21 | return 'TypePlains' 22 | elif 'hills' in name: 23 | return 'TypeHills' 24 | elif 'desert' in name: 25 | return 'TypeDesert' 26 | elif 'forest' in name: 27 | return 'TypeForest' 28 | elif 'taiga' in name: 29 | return 'TypeTaiga' 30 | elif 'swamp' in name: 31 | return 'TypeSwamp' 32 | elif 'river' in name: 33 | return 'TypeRiver' 34 | elif 'nether' in name: 35 | return 'TypeNether' 36 | elif 'end' in name: 37 | return 'TypeSky' 38 | elif 'snow' in name: 39 | return 'TypeSnow' 40 | elif 'beach' in name: 41 | return 'TypeBeach' 42 | elif 'mushroom' in name: 43 | return 'TypeMushroomIsland' 44 | elif 'jungle' in name: 45 | return 'TypeJungle' 46 | elif 'savanna' in name: 47 | return 'TypeSavanna' 48 | elif 'badlands' in name: 49 | return 'TypeMesa' 50 | 51 | elif 'stone_shore' == name: 52 | return 'TypeStoneBeach' 53 | 54 | elif 'the_void' == name: 55 | return 'TypeVoid' 56 | 57 | elif 'ice_spikes' == name: 58 | return 'TypeSnow' 59 | 60 | # must be last so it will get the most correct of all 61 | elif 'mountain' in name: 62 | return 'TypeHills' 63 | 64 | else: 65 | assert False, f"Could not figure out type for {name}" 66 | 67 | 68 | print("// Code generated by scripts/generate_biomes.go; DO NOT EDIT.") 69 | print("// This file was generated by robots at") 70 | print("// " + str(datetime.now())) 71 | print() 72 | 73 | print("package biome") 74 | print() 75 | 76 | # Because the world gen uses biomes in a very special 77 | # way we are going to create variable for each biome 78 | # type for ease of use 79 | 80 | for biome in biomes: 81 | name = stringcase.pascalcase(biome['name']) 82 | print(f"var {name} = &Biome{{") 83 | print(f"\tId: {biome['id']},") 84 | print(f"\tName: \"{biome['name']}\",") 85 | print(f"\tType: {figure_biome_type(biome['name'])},") 86 | print(f"\tRainfall: {biome['rainfall']},") 87 | print(f"\tTemperature: {biome['temperature']},") 88 | print("}") 89 | 90 | print() 91 | 92 | # Biome list by id 93 | print("var biomes = [...]*Biome{") 94 | for biome in biomes: 95 | name = stringcase.pascalcase(biome['name']) 96 | print(f"\t{biome['id']}: {name},") 97 | print("}") 98 | print("") 99 | -------------------------------------------------------------------------------- /scripts/generate_blocks.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from datetime import datetime 3 | import os.path as path 4 | import urllib.request 5 | import json 6 | 7 | # TODO: switch to using go:generate 8 | 9 | # This has all of the data we need 10 | import stringcase 11 | 12 | with urllib.request.urlopen("https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.15.2/blocks.json") as f: 13 | blocks = json.load(f) 14 | 15 | with urllib.request.urlopen("https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.15.2/items.json") as f: 16 | items = json.load(f) 17 | 18 | 19 | block_by_name = {} 20 | for block in blocks: 21 | block_by_name[block['name']] = block 22 | 23 | item_by_name = {} 24 | for item in items: 25 | item_by_name[item['name']] = item 26 | 27 | 28 | print("// Code generated by scripts/generate_blocks.go; DO NOT EDIT.") 29 | print("// This file was generated by robots at") 30 | print("// " + str(datetime.now())) 31 | print() 32 | 33 | print("package block") 34 | print() 35 | print('import "github.com/itay2805/mcserver/minecraft/item"') 36 | print() 37 | 38 | # Block variables 39 | for block in blocks: 40 | name = stringcase.pascalcase(block['name']) 41 | print(f"var {name} = &Block{{") 42 | print(f"\tId: {block['id']},") 43 | print(f"\tName: \"{block['name']}\",") 44 | print(f"\tMinStateId: {block['minStateId']},") 45 | print(f"\tMaxStateId: {block['maxStateId']},") 46 | print(f"\tDefaultStateId: {block['defaultState']},") 47 | print(f"\tSolid: {'false' if block['boundingBox'] == 'empty' else 'true'},") 48 | print(f"\tTransparent: {'true' if block['transparent'] else 'false'},") 49 | print(f"\tFilterLight: {block['filterLight']},") 50 | print(f"\tEmitLight: {block['emitLight']},") 51 | if block['name'] in item_by_name: 52 | print(f"\tItem: item.{name},") 53 | print("}") 54 | print() 55 | 56 | # Block by id lookup 57 | print("var blocks = [...]*Block{") 58 | for block in blocks: 59 | name = stringcase.pascalcase(block['name']) 60 | print(f"\t{block['id']}: {name},") 61 | print("}") 62 | print("") 63 | 64 | # Block by state id lookup 65 | print("var stateIdToBlockId = [...]*Block{") 66 | for block in blocks: 67 | for i in range(block['minStateId'], block['maxStateId'] + 1): 68 | print(f"\tblocks[{block['id']}],") 69 | print("}") 70 | print("") 71 | 72 | # Block by item id 73 | print("var blockByItemId = [...]*Block{") 74 | for item in items: 75 | if item['name'] in block_by_name: 76 | name = stringcase.pascalcase(item['name']) 77 | print(f"\t{item['id']}: {name},") 78 | print("}") 79 | print("") 80 | -------------------------------------------------------------------------------- /scripts/generate_entities.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from datetime import datetime 3 | import os.path as path 4 | import urllib.request 5 | import json 6 | 7 | # TODO: switch to using go:generate 8 | 9 | # This has all of the data we need 10 | with urllib.request.urlopen("https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.15.2/entities.json") as f: 11 | entities = json.load(f) 12 | 13 | print("// Code generated by scripts/generate_entities.go; DO NOT EDIT.") 14 | print("// This file was generated by robots at") 15 | print("// " + str(datetime.now())) 16 | print() 17 | 18 | print("package entity") 19 | print() 20 | print("var types = [...]Type{") 21 | for entity in entities: 22 | print(f"\t{entity['id']}: {{") 23 | print(f"\t\tId: {entity['id']},") 24 | print(f"\t\tName: \"{entity['name']}\",") 25 | print(f"\t\tWidth: {entity['width']},") 26 | print(f"\t\tHeight: {entity['height']},") 27 | print("\t},") 28 | print("}") 29 | print() 30 | print("var byName = map[string]*Type{") 31 | for entity in entities: 32 | print(f"\t\"{entity['name']}\": &types[{entity['id']}],") 33 | print("}") 34 | print() 35 | -------------------------------------------------------------------------------- /scripts/generate_items.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from datetime import datetime 3 | import os.path as path 4 | import urllib.request 5 | import json 6 | 7 | # TODO: switch to using go:generate 8 | 9 | # This has all of the data we need 10 | import stringcase 11 | 12 | with urllib.request.urlopen("https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/1.15.2/items.json") as f: 13 | items = json.load(f) 14 | 15 | print("// Code generated by scripts/generate_items.go; DO NOT EDIT.") 16 | print("// This file was generated by robots at") 17 | print("// " + str(datetime.now())) 18 | print() 19 | 20 | print("package item") 21 | print() 22 | 23 | # Item variables 24 | for item in items: 25 | name = stringcase.pascalcase(item['name']) 26 | print(f"var {name} = &Item{{") 27 | print(f"\tID: {item['id']},") 28 | print(f"\tName: \"{item['name']}\",") 29 | print(f"\tStackSize: {item['stackSize']},") 30 | print("}") 31 | print() 32 | 33 | # Item by id lookup 34 | print("var items = [...]*Item{") 35 | for item in items: 36 | name = stringcase.pascalcase(item['name']) 37 | print(f"\t{item['id']}: {name},") 38 | print("}") 39 | print() 40 | -------------------------------------------------------------------------------- /scripts/generate_meta.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from datetime import datetime 3 | import os.path as path 4 | import urllib.request 5 | import json 6 | 7 | # TODO: switch to using go:generate 8 | 9 | # This has all of the data we need 10 | import stringcase 11 | 12 | enums = { 13 | # 4-way facing 14 | ( 15 | "north", 16 | "south", 17 | "west", 18 | "east" 19 | ): { 20 | 'type': 'minecraft.Face', 21 | 'to_meta': 'meta += uint16({value}) - 2', 22 | 'from_meta': '{name} = minecraft.Face((meta % 4) + 2)' 23 | }, 24 | 25 | # 6-way facing 26 | # ( 27 | # "north", 28 | # "south", 29 | # "west", 30 | # "east" 31 | # ): { 32 | # 'type': 'minecraft.Face', 33 | # 'to_meta': 'meta += int({value}) - 2', 34 | # 'from_meta': 'minecraft.Face((meta % 4) + 2)' 35 | # }, 36 | 37 | # A shape 38 | ( 39 | "straight", 40 | "inner_left", 41 | "inner_right", 42 | "outer_left", 43 | "outer_right" 44 | ): { 45 | 'type': 'minecraft.Shape', 46 | }, 47 | 48 | # 2-way face (top bottom) 49 | ( 50 | "top", 51 | "bottom" 52 | ): { 53 | 'type': 'minecraft.Face', 54 | }, 55 | 56 | # Same as above 57 | ( 58 | "upper", 59 | "lower" 60 | ): { 61 | 'type': 'Half', 62 | }, 63 | 64 | # left-right directions 65 | ( 66 | "left", 67 | "right" 68 | ): { 69 | 'type': 'Hinge', 70 | } 71 | } 72 | 73 | # The meta generation 74 | metas = [ 75 | # Used for any stair kind 76 | { 77 | 'name': 'StairsMeta', 78 | 'states': [ 79 | { 80 | "name": "facing", 81 | "type": "enum", 82 | "num_values": 4, 83 | "values": [ 84 | "north", 85 | "south", 86 | "west", 87 | "east" 88 | ] 89 | }, 90 | { 91 | "name": "half", 92 | "type": "enum", 93 | "num_values": 2, 94 | "values": [ 95 | "top", 96 | "bottom" 97 | ] 98 | }, 99 | { 100 | "name": "shape", 101 | "type": "enum", 102 | "num_values": 5, 103 | "values": [ 104 | "straight", 105 | "inner_left", 106 | "inner_right", 107 | "outer_left", 108 | "outer_right" 109 | ] 110 | }, 111 | { 112 | "name": "waterlogged", 113 | "type": "bool", 114 | "num_values": 2 115 | } 116 | ] 117 | }, 118 | 119 | { 120 | 'name': 'FurnaceMeta', 121 | "states": [ 122 | { 123 | "name": "facing", 124 | "type": "enum", 125 | "num_values": 4, 126 | "values": [ 127 | "north", 128 | "south", 129 | "west", 130 | "east" 131 | ] 132 | }, 133 | { 134 | "name": "lit", 135 | "type": "bool", 136 | "num_values": 2 137 | } 138 | ], 139 | }, 140 | 141 | # { 142 | # 'name': 'BarrelMeta', 143 | # "states": [ 144 | # { 145 | # "name": "facing", 146 | # "type": "enum", 147 | # "num_values": 6, 148 | # "values": [ 149 | # "north", 150 | # "east", 151 | # "south", 152 | # "west", 153 | # "up", 154 | # "down" 155 | # ] 156 | # }, 157 | # { 158 | # "name": "open", 159 | # "type": "bool", 160 | # "num_values": 2 161 | # } 162 | # ], 163 | # } 164 | ] 165 | 166 | print("// Code generated by scripts/generate_meta.go; DO NOT EDIT.") 167 | print("// This file was generated by robots at") 168 | print("// " + str(datetime.now())) 169 | print() 170 | 171 | print("package block") 172 | print() 173 | 174 | for meta in metas: 175 | name = stringcase.pascalcase(meta['name']) 176 | print(f'type {name} struct {{') 177 | for state in meta['states']: 178 | nam = stringcase.pascalcase(state['name']) 179 | typ = state['type'] 180 | if typ == 'int': 181 | print(f'\t{nam} int') 182 | elif typ == 'enum': 183 | print(f'\t{nam} {enums[tuple(state["values"])]["type"]}') 184 | elif typ == 'bool': 185 | print(f'\t{nam} bool') 186 | print('}') 187 | print() 188 | 189 | print(f'func (m *{name}) FromMeta(meta uint16) {{') 190 | for state in reversed(meta['states']): 191 | nam = stringcase.pascalcase(state['name']) 192 | if 'enum' == state['type']: 193 | n = enums[tuple(state["values"])] 194 | if 'from_meta' in n: 195 | print('\t' + n["from_meta"].format(name=f"m.{nam}")) 196 | else: 197 | print(f'\tm.{nam} = {n["type"]}(meta % {state["num_values"]})') 198 | elif 'bool' == state['type']: 199 | print(f'\tm.{nam} = (meta % 2) == 1') 200 | else: 201 | print(f'\tm.{nam} = meta % {state["num_values"]}') 202 | print(f'\tmeta /= {state["num_values"]}') 203 | print('}') 204 | print() 205 | 206 | print(f'func (m *{name}) ToMeta() uint16 {{') 207 | print('\tmeta := uint16(0)') 208 | for state in meta['states']: 209 | nam = stringcase.pascalcase(state['name']) 210 | print(f'\tmeta *= {state["num_values"]}') 211 | if 'enum' == state['type']: 212 | n = enums[tuple(state["values"])] 213 | if 'to_meta' in n: 214 | print('\t' + n["to_meta"].format(value=f"m.{nam}")) 215 | else: 216 | print(f'\tmeta += uint16(m.{nam})') 217 | elif 'bool' == state['type']: 218 | print(f'\tmeta += boolToMeta(m.{nam})') 219 | else: 220 | print(f'\tmeta += m.{nam}') 221 | print('\treturn meta') 222 | print('}') 223 | print() 224 | print() 225 | -------------------------------------------------------------------------------- /server/handshaking/handlers.go: -------------------------------------------------------------------------------- 1 | package handshaking 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/game" 5 | "github.com/itay2805/mcserver/minecraft" 6 | "github.com/itay2805/mcserver/minecraft/proto/login" 7 | "github.com/itay2805/mcserver/server/socket" 8 | "log" 9 | ) 10 | 11 | func HandleHandshaking(player *game.Player, reader *minecraft.Reader) { 12 | protocolVersion := reader.ReadVarint() 13 | _ = reader.ReadString(255) 14 | _ = reader.ReadUShort() 15 | nextState := reader.ReadVarint() 16 | 17 | if nextState == 1 { 18 | // go to status, no need for protocol version check 19 | player.State = socket.Status 20 | 21 | } else if nextState == 2 { 22 | // go to login, only if the protocol version is good 23 | if protocolVersion != 578 { 24 | log.Println("Client at", player.RemoteAddr(), "tried to login from invalid client", protocolVersion, "(expected 340)") 25 | 26 | // kick the player 27 | player.Send(login.Disconnect{ 28 | Reason: minecraft.Chat{ 29 | Text: "Invalid protocol version", 30 | Bold: true, 31 | Italic: true, 32 | Underlined: true, 33 | Strikethrough: true, 34 | Color: "red", 35 | }, 36 | }) 37 | 38 | // set the player as disconnected 39 | player.State = socket.Disconnected 40 | } else { 41 | // move the player to login state 42 | player.State = socket.Login 43 | } 44 | 45 | } else { 46 | log.Panicln("Requested invalid state", nextState) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/login/handlers.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "crypto/md5" 5 | "github.com/google/uuid" 6 | "github.com/itay2805/mcserver/game" 7 | "github.com/itay2805/mcserver/minecraft" 8 | "github.com/itay2805/mcserver/minecraft/entity" 9 | "github.com/itay2805/mcserver/minecraft/proto/login" 10 | "github.com/itay2805/mcserver/server/socket" 11 | "log" 12 | ) 13 | 14 | func offlineUUID(name string) uuid.UUID { 15 | var version = 3 16 | h := md5.New() 17 | h.Reset() 18 | h.Write([]byte("OfflinePlayer:" + name)) 19 | s := h.Sum(nil) 20 | var uuid uuid.UUID 21 | copy(uuid[:], s) 22 | uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) 23 | uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant 24 | return uuid 25 | } 26 | 27 | func HandleLoginStart(player *game.Player, reader *minecraft.Reader) { 28 | name := reader.ReadString(16) 29 | log.Println("Got login request from", player.RemoteAddr(), "connecting from", player.RemoteAddr()) 30 | 31 | uuid := offlineUUID(name) 32 | 33 | // Set Compression (block until done) 34 | player.SendSync(login.SetCompression{Threshold: 128}) 35 | 36 | // sleep a bit before enabling compression so we will 37 | // send the packet without compression 38 | player.EnableCompression() 39 | 40 | // create the player object 41 | player.Player = entity.NewPlayer(name, uuid) 42 | 43 | // now we can actually send the login success 44 | player.Send(login.LoginSuccess{ 45 | Uuid: uuid, 46 | Username: name, 47 | }) 48 | 49 | // we are not in play state 50 | player.State = socket.Play 51 | game.JoinPlayer(player) 52 | } 53 | -------------------------------------------------------------------------------- /server/play/menu.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "errors" 5 | "github.com/itay2805/mcserver/game" 6 | "github.com/itay2805/mcserver/minecraft" 7 | "github.com/itay2805/mcserver/minecraft/proto/play" 8 | "log" 9 | ) 10 | 11 | func HandleHeldItemChange(player *game.Player, reader *minecraft.Reader) { 12 | slot := reader.ReadShort() 13 | 14 | player.Change(func() { 15 | player.ChangeHeldItem(int(slot)) 16 | }) 17 | } 18 | 19 | 20 | func HandleCreativeInventoryAction(player *game.Player, reader *minecraft.Reader) { 21 | // TODO: Check for creative mode 22 | 23 | slot := reader.ReadShort() 24 | 25 | if reader.ReadBoolean() { 26 | itemId := reader.ReadVarint() 27 | itemCount := reader.ReadByte() 28 | var nbt interface{} 29 | 30 | err := minecraft.NewNbtDecoder(reader).Decode(&nbt) 31 | if errors.Is(err, minecraft.ErrEND) { 32 | nbt = nil 33 | } else if err != nil { 34 | log.Panicln(err) 35 | } 36 | 37 | newSlot := &play.Slot{ 38 | ItemID: itemId, 39 | ItemCount: itemCount, 40 | NBT: nbt, 41 | } 42 | 43 | 44 | player.Change(func() { 45 | player.UpdateInventory(int(slot), newSlot) 46 | }) 47 | } else { 48 | player.Change(func() { 49 | player.UpdateInventory(int(slot), nil) 50 | }) 51 | } 52 | } -------------------------------------------------------------------------------- /server/play/player.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/config" 5 | "github.com/itay2805/mcserver/game" 6 | "github.com/itay2805/mcserver/minecraft" 7 | "github.com/itay2805/mcserver/minecraft/proto/play" 8 | "log" 9 | "time" 10 | ) 11 | 12 | func HandleClientSettings(player *game.Player, reader *minecraft.Reader) { 13 | _ = reader.ReadString(16) // locale 14 | viewDistance := reader.ReadByte() 15 | _ = reader.ReadVarint() // Chat Mode 16 | _ = reader.ReadBoolean() // Chat colors 17 | skinMask := reader.ReadByte() 18 | mainHand := reader.ReadVarint() 19 | 20 | // max view distance 21 | if viewDistance > byte(*config.MaxViewDistance) { 22 | viewDistance = byte(*config.MaxViewDistance) 23 | } 24 | 25 | player.Change(func() { 26 | player.ViewDistance = int(viewDistance) 27 | player.SkinMask = skinMask 28 | player.MainHand = byte(mainHand) 29 | player.MetadataChanged = true 30 | }) 31 | } 32 | 33 | func HandleKeepAlive(player *game.Player, reader *minecraft.Reader) { 34 | gotIt := time.Now() 35 | keepAliveId := reader.ReadLong() 36 | player.Change(func() { 37 | player.Ping = gotIt.Sub(time.Unix(0, keepAliveId)) 38 | player.PingChanged = true 39 | }) 40 | } 41 | 42 | func HandlePlayerPosition(player *game.Player, reader *minecraft.Reader) { 43 | x := reader.ReadDouble() 44 | y := reader.ReadDouble() 45 | z := reader.ReadDouble() 46 | onGround := reader.ReadBoolean() 47 | player.Change(func() { 48 | player.PrevPosition = player.Position 49 | 50 | prevX := player.Position[0] 51 | prevY := player.Position[1] 52 | prevZ := player.Position[2] 53 | 54 | player.Position[0] = x 55 | player.Position[1] = y 56 | player.Position[2] = z 57 | player.OnGround = onGround 58 | 59 | player.Moved = prevX != x || prevY != y || prevZ != z 60 | }) 61 | } 62 | 63 | func HandlePlayerPositionAndRotation(player *game.Player, reader *minecraft.Reader) { 64 | x := reader.ReadDouble() 65 | y := reader.ReadDouble() 66 | z := reader.ReadDouble() 67 | yaw := minecraft.ToAngle(reader.ReadFloat()) 68 | pitch := minecraft.ToAngle(reader.ReadFloat()) 69 | onGround := reader.ReadBoolean() 70 | player.Change(func() { 71 | prevX := player.Position[0] 72 | prevY := player.Position[1] 73 | prevZ := player.Position[2] 74 | prevYaw := player.Yaw 75 | prevPitch := player.Pitch 76 | 77 | player.Position[0] = x 78 | player.Position[1] = y 79 | player.Position[2] = z 80 | player.Yaw = yaw 81 | player.HeadYaw = yaw 82 | player.Pitch = pitch 83 | player.OnGround = onGround 84 | 85 | player.Moved = prevX != x || prevY != y || prevZ != z 86 | player.Rotated = pitch != prevPitch || yaw != prevYaw 87 | }) 88 | 89 | } 90 | 91 | func HandlePlayerRotation(player *game.Player, reader *minecraft.Reader) { 92 | yaw := minecraft.ToAngle(reader.ReadFloat()) 93 | pitch := minecraft.ToAngle(reader.ReadFloat()) 94 | onGround := reader.ReadBoolean() 95 | player.Change(func() { 96 | prevYaw := player.Yaw 97 | prevPitch := player.Pitch 98 | 99 | player.Yaw = yaw 100 | player.Pitch = pitch 101 | player.HeadYaw = yaw 102 | player.OnGround = onGround 103 | 104 | player.Rotated = pitch != prevPitch || yaw != prevYaw 105 | }) 106 | } 107 | 108 | func HandlePlayerMovement(player *game.Player, reader *minecraft.Reader) { 109 | onGround := reader.ReadBoolean() 110 | player.Change(func() { 111 | player.OnGround = onGround 112 | }) 113 | } 114 | 115 | func HandlePlayerAbilities(player *game.Player, reader *minecraft.Reader) { 116 | flags := reader.ReadByte() 117 | 118 | player.Change(func() { 119 | player.Flying = (flags & 0x2) != 0 120 | }) 121 | } 122 | 123 | func HandlePlayerDigging(player *game.Player, reader *minecraft.Reader) { 124 | status := reader.ReadVarint() 125 | location := reader.ReadPosition() 126 | face := reader.ReadByte() 127 | 128 | switch status { 129 | // queue a player dig action 130 | case 0: 131 | // register that the player digged 132 | player.Change(func() { 133 | player.ActionQueue.Add(game.PlayerAction{ 134 | Type: game.PlayerActionDig, 135 | Data: location, 136 | }) 137 | }) 138 | 139 | default: 140 | log.Println("HandlePlayerDigging:", player, "unknwon status", status, "at", location, "fact", face) 141 | } 142 | } 143 | 144 | func HandleEntityAction(player *game.Player, reader *minecraft.Reader) { 145 | eid := reader.ReadVarint() 146 | aid := reader.ReadVarint() 147 | 148 | if eid != player.EID { 149 | log.Println("HandleEntityAction:", player, "sent invalid entity id", eid, "(expected", player.EID, ")") 150 | return 151 | } 152 | 153 | switch aid { 154 | case 0: 155 | player.Change(func() { 156 | player.Pose = minecraft.PoseSneaking 157 | player.MetadataChanged = true 158 | }) 159 | case 1: 160 | player.Change(func() { 161 | if player.Pose == minecraft.PoseSneaking { 162 | player.Pose = minecraft.PoseStanding 163 | player.MetadataChanged = true 164 | } 165 | }) 166 | case 3: 167 | player.Change(func() { 168 | player.Sprinting = true 169 | player.MetadataChanged = true 170 | }) 171 | case 4: 172 | player.Change(func() { 173 | player.Sprinting = false 174 | player.MetadataChanged = true 175 | }) 176 | default: 177 | log.Println("HandleEntityAction:", player, "sent invalid action id", aid) 178 | } 179 | } 180 | 181 | func HandleAnimation(player *game.Player, reader *minecraft.Reader) { 182 | hand := reader.ReadVarint() 183 | switch hand { 184 | case 0: 185 | player.Change(func() { 186 | player.Animation = play.AnimationSwingMainHand 187 | }) 188 | case 1: 189 | player.Change(func() { 190 | player.Animation = play.AnimationSwingOffhand 191 | }) 192 | default: 193 | log.Println("HandleAnimation:", player, "unknown hand", hand) 194 | } 195 | } 196 | 197 | func HandlePlayerBlockPlacement(player *game.Player, reader *minecraft.Reader) { 198 | hand := reader.ReadVarint() 199 | location := reader.ReadPosition() 200 | face := minecraft.Face(reader.ReadVarint()) 201 | cursorPositionX := reader.ReadFloat() 202 | cursorPositionY := reader.ReadFloat() 203 | cursorPositionZ := reader.ReadFloat() 204 | _ = reader.ReadBoolean() 205 | 206 | data := game.BlockPlacement{ 207 | Hand: hand, 208 | Location: location, 209 | Face: face, 210 | CursorPositionX: cursorPositionX, 211 | CursorPositionY: cursorPositionY, 212 | CursorPositionZ: cursorPositionZ, 213 | } 214 | 215 | player.Change(func() { 216 | player.ActionQueue.Add(game.PlayerAction{ 217 | Type: game.PlayerActionPlace, 218 | Data: data, 219 | }) 220 | }) 221 | } 222 | 223 | func HandleUseItem(player *game.Player, reader *minecraft.Reader) { 224 | hand := reader.ReadVarint() 225 | player.Change(func() { 226 | player.ActionQueue.Add(game.PlayerAction{ 227 | Type: game.PlayerActionUseItem, 228 | Data: hand, 229 | }) 230 | }) 231 | } 232 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "errors" 5 | "github.com/itay2805/mcserver/game" 6 | "github.com/itay2805/mcserver/minecraft" 7 | "github.com/itay2805/mcserver/server/handshaking" 8 | "github.com/itay2805/mcserver/server/login" 9 | "github.com/itay2805/mcserver/server/play" 10 | "github.com/itay2805/mcserver/server/socket" 11 | "github.com/itay2805/mcserver/server/status" 12 | "github.com/panjf2000/ants" 13 | "io" 14 | "log" 15 | "net" 16 | "runtime/debug" 17 | ) 18 | 19 | type PacketHandler func(player *game.Player, reader *minecraft.Reader) 20 | 21 | var packetHandlers = [][]PacketHandler { 22 | socket.Handshaking: { 23 | 0x00: handshaking.HandleHandshaking, 24 | }, 25 | socket.Status: { 26 | 0x00: status.HandleRequest, 27 | 0x01: status.HandlePing, 28 | }, 29 | socket.Login: { 30 | 0x00: login.HandleLoginStart, 31 | }, 32 | socket.Play: { 33 | // 0x00 34 | // 0x01 35 | // 0x02 36 | // 0x03 37 | // 0x04 38 | 0x05: play.HandleClientSettings, 39 | // 0x06 40 | // 0x07 41 | // 0x08 42 | // 0x09 43 | // 0x0A 44 | // 0x0B 45 | // 0x0C 46 | // 0x0D 47 | // 0x0E 48 | 0x0F: play.HandleKeepAlive, 49 | //0x10: 50 | 0x11: play.HandlePlayerPosition, 51 | 0x12: play.HandlePlayerPositionAndRotation, 52 | 0x13: play.HandlePlayerRotation, 53 | 0x14: play.HandlePlayerMovement, 54 | // 0x15 55 | // 0x16 56 | // 0x17 57 | // 0x18 58 | 0x19: play.HandlePlayerAbilities, 59 | 0x1A: play.HandlePlayerDigging, 60 | 0x1B: play.HandleEntityAction, 61 | // 0x1C 62 | // 0x1D 63 | // 0x1E 64 | // 0x1F 65 | // 0x20 66 | // 0x21 67 | // 0x22 68 | 0x23: play.HandleHeldItemChange, 69 | // 0x24 70 | // 0x25 71 | 0x26: play.HandleCreativeInventoryAction, 72 | // 0x27 73 | // 0x28 74 | // 0x29 75 | 0x2A: play.HandleAnimation, 76 | // 0x2B 77 | 0x2C: play.HandlePlayerBlockPlacement, 78 | 0x2D: play.HandleUseItem, 79 | }, 80 | } 81 | 82 | func handleConnection(conn net.Conn) { 83 | // create the net player, which will be used 84 | // for the entity and for the socket 85 | player := game.NewPlayer(socket.NewSocket(conn)) 86 | 87 | // do cleanup in here, this will make sure so even if we 88 | // get a panic in an handler everything will still work 89 | defer func() { 90 | // remove the player 91 | if player.State == socket.Play { 92 | player.State = socket.Disconnected 93 | game.LeftPlayer(player) 94 | } 95 | 96 | // close the connection 97 | player.Close() 98 | 99 | // recover from error 100 | if r := recover(); r != nil { 101 | log.Println("Got error", r, "\n" + string(debug.Stack())) 102 | } 103 | }() 104 | 105 | // start the send goroutine for this client 106 | go player.StartSend() 107 | 108 | // start the recv loop which will recv 109 | // packets and handle them correctly 110 | for { 111 | data, err := player.Recv() 112 | if err != nil { 113 | if errors.Is(err, io.EOF) { 114 | // socket closed, ignore 115 | } else { 116 | log.Println("Got an error recving packet:", err) 117 | } 118 | break 119 | } 120 | 121 | reader := minecraft.Reader{ 122 | Data: data, 123 | } 124 | packetId := reader.ReadVarint() 125 | 126 | // check the id is even valid 127 | handlers := packetHandlers[player.State] 128 | if len(handlers) <= int(packetId) { 129 | log.Println("Got invalid packet id", packetId, "from", player, "state", player.State) 130 | continue 131 | } 132 | 133 | // check the handler is all good and call it if so 134 | if handlers[packetId] != nil { 135 | handlers[packetId](player, &reader) 136 | } else { 137 | log.Println("Got invalid packet id", packetId, "from", player, "state", player.State) 138 | continue 139 | } 140 | 141 | // check if we need to disconnect 142 | if player.State == socket.Disconnected { 143 | break 144 | } 145 | } 146 | } 147 | 148 | func StartServer() { 149 | log.Println("Starting server on :25565") 150 | 151 | // we are going to use ants for sending shit 152 | defer ants.Release() 153 | 154 | // 155 | // start the accept loop 156 | // 157 | server, err := net.Listen("tcp4", ":25565") 158 | if err != nil { 159 | log.Panicln("Got error listening:", err) 160 | } 161 | 162 | for { 163 | // accept a connection 164 | conn, err := server.Accept() 165 | if err != nil { 166 | log.Panicln("Got error accepting connection:", err) 167 | } 168 | 169 | // start the connection handling in 170 | // a new goroutine 171 | go handleConnection(conn) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /server/socket/socket.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/zlib" 7 | "errors" 8 | "github.com/itay2805/mcserver/common" 9 | "github.com/itay2805/mcserver/minecraft" 10 | "io" 11 | "log" 12 | "net" 13 | ) 14 | 15 | type Packet interface { 16 | Encode(writer *minecraft.Writer) 17 | } 18 | 19 | const ( 20 | Disconnected = -1 21 | Handshaking = 0 22 | Status = 1 23 | Login = 2 24 | Play = 3 25 | ) 26 | 27 | 28 | type Socket struct { 29 | net.Conn 30 | 31 | // the protocol state 32 | State int 33 | 34 | // the reader and writer 35 | // for this socket 36 | reader *bufio.Reader 37 | writer io.Writer 38 | 39 | // the send queues 40 | // TODO: maybe don't use this... 41 | sendQueueIn chan<- interface{} 42 | sendQueueOut <-chan interface{} 43 | sendDone chan bool 44 | 45 | // The buffer used for recving data 46 | recvBuffer [65565]byte 47 | decompBuffer [65565]byte 48 | 49 | // Compression threshold 50 | compression bool 51 | } 52 | 53 | func NewSocket(conn net.Conn) Socket { 54 | in, out := common.MakeInfinite() 55 | return Socket{ 56 | Conn: conn, 57 | State: Handshaking, 58 | reader: bufio.NewReader(conn), 59 | writer: conn, 60 | sendQueueIn: in, 61 | sendQueueOut: out, 62 | sendDone: make(chan bool), 63 | recvBuffer: [65565]byte{}, 64 | decompBuffer: [65565]byte{}, 65 | compression: false, 66 | } 67 | } 68 | 69 | func (s*Socket) EnableCompression() { 70 | s.compression = true 71 | } 72 | 73 | func (s *Socket) readVarint() (int, error) { 74 | numRead := 0 75 | result := 0 76 | for { 77 | read, err := s.reader.ReadByte() 78 | if err != nil { 79 | return 0, err 80 | } 81 | value := read & 0b01111111 82 | result |= int(value) << (7 * numRead) 83 | 84 | numRead++ 85 | if numRead > 5 { 86 | return 0, errors.New("varint is too big") 87 | } 88 | 89 | if (read & 0b10000000) == 0 { 90 | return result, nil 91 | } 92 | } 93 | } 94 | 95 | func (s *Socket) Recv() ([]byte, error) { 96 | // read the length 97 | packetLength, err := s.readVarint() 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | // read the full data of the packet 103 | _, err = io.ReadFull(s.reader, s.recvBuffer[:packetLength]) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | if s.compression { 109 | reader := minecraft.Reader{Data: s.recvBuffer[:packetLength]} 110 | dataLength := reader.ReadVarint() 111 | 112 | if dataLength != 0 { 113 | // compressed 114 | r, err := zlib.NewReader(&reader) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | // recompress it fully 120 | _, err = io.ReadFull(r, s.decompBuffer[:dataLength]) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | // return the uncompressed data 126 | return s.decompBuffer[:dataLength], nil 127 | } else { 128 | // uncompressed, skip the length byte 129 | return s.recvBuffer[1:packetLength], nil 130 | } 131 | } else { 132 | // return the packet 133 | return s.recvBuffer[:packetLength], nil 134 | } 135 | } 136 | 137 | func (s *Socket) writeVarint(val int) error { 138 | data := [5]byte{} 139 | offset := 0 140 | 141 | temp := byte(0) 142 | for { 143 | temp = byte(val & 0b01111111) 144 | val >>= 7 145 | if val != 0 { 146 | temp |= 0b10000000 147 | } 148 | data[offset] = temp 149 | offset++ 150 | if val == 0 { 151 | break 152 | } 153 | } 154 | 155 | _, err := s.Write(data[:offset]) 156 | return err 157 | } 158 | 159 | type sendRequest struct { 160 | data []byte 161 | done chan bool 162 | } 163 | 164 | func (s *Socket) Send(packet Packet) { 165 | writer := minecraft.Writer{} 166 | packet.Encode(&writer) 167 | s.SendRaw(writer.Bytes()) 168 | } 169 | 170 | func (s *Socket) SendRaw(data []byte) { 171 | s.SendRawChan(data, nil) 172 | } 173 | 174 | func (s *Socket) SendSync(packet Packet) { 175 | writer := minecraft.Writer{} 176 | packet.Encode(&writer) 177 | s.SendRawSync(writer.Bytes()) 178 | } 179 | 180 | func (s *Socket) SendRawSync(data []byte) { 181 | done := make(chan bool) 182 | s.SendRawChan(data, done) 183 | <-done 184 | } 185 | 186 | func (s *Socket) SendChan(packet Packet, done chan bool) { 187 | writer := minecraft.Writer{} 188 | packet.Encode(&writer) 189 | s.SendRawChan(writer.Bytes(), done) 190 | } 191 | 192 | func (s *Socket) SendRawChan(data []byte, done chan bool) { 193 | s.sendQueueIn <- sendRequest{ 194 | data: data, 195 | done: done, 196 | } 197 | } 198 | 199 | func (s *Socket) StartSend() { 200 | buf := bytes.Buffer{} 201 | 202 | for req := range s.sendQueueOut { 203 | data := req.(sendRequest) 204 | 205 | if s.compression { 206 | if len(data.data) > 128 { 207 | // attempt to compress 208 | buf.Reset() 209 | w := zlib.NewWriter(&buf) 210 | _, _ = w.Write(data.data) 211 | _ = w.Close() 212 | 213 | compressed := buf.Bytes()[:buf.Len()] 214 | 215 | // write the size of compressed + data size 216 | err := s.writeVarint( 217 | common.VarintSize(int32(len(data.data))) + len(compressed), 218 | ) 219 | if err != nil { 220 | log.Println("go error", err, "on", s.RemoteAddr()) 221 | goto gotError 222 | } 223 | 224 | // write the data size 225 | err = s.writeVarint(len(data.data)) 226 | if err != nil { 227 | log.Println("go error", err, "on", s.RemoteAddr()) 228 | goto gotError 229 | } 230 | 231 | // write the data 232 | _, err = s.Write(compressed) 233 | if err != nil { 234 | log.Println("go error", err, "on", s.RemoteAddr()) 235 | goto gotError 236 | } 237 | 238 | // skip the rest of the code, we are done 239 | } else { 240 | // don't compress 241 | // write the packet length 242 | err := s.writeVarint(len(data.data) + 1) 243 | if err != nil { 244 | log.Println("go error", err, "on", s.RemoteAddr()) 245 | goto gotError 246 | } 247 | 248 | // set no compression 249 | err = s.writeVarint(0) 250 | if err != nil { 251 | log.Println("go error", err, "on", s.RemoteAddr()) 252 | goto gotError 253 | } 254 | 255 | // write the data 256 | _, err = s.Write(data.data) 257 | if err != nil { 258 | log.Println("go error", err, "on", s.RemoteAddr()) 259 | goto gotError 260 | } 261 | } 262 | } else { 263 | err := s.writeVarint(len(data.data)) 264 | if err != nil { 265 | log.Println("go error", err, "on", s.RemoteAddr()) 266 | goto gotError 267 | } 268 | 269 | _, err = s.Write(data.data) 270 | if err != nil { 271 | log.Println("go error", err, "on", s.RemoteAddr()) 272 | goto gotError 273 | } 274 | } 275 | 276 | if data.done != nil { 277 | data.done <- true 278 | } 279 | continue 280 | 281 | gotError: 282 | data.done <- false 283 | break 284 | } 285 | 286 | s.sendDone <- true 287 | } 288 | 289 | 290 | func (s *Socket) Close() { 291 | // close the queue in, which will close 292 | // the send queue 293 | close(s.sendQueueIn) 294 | 295 | // wait for all the packets that need to 296 | // be sent to be sent 297 | <-s.sendDone 298 | 299 | // now close the connection itself 300 | _ = s.Conn.Close() 301 | } 302 | -------------------------------------------------------------------------------- /server/status/handlers.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "github.com/itay2805/mcserver/game" 5 | "github.com/itay2805/mcserver/minecraft" 6 | "github.com/itay2805/mcserver/minecraft/proto/status" 7 | "math" 8 | ) 9 | 10 | func HandleRequest(player *game.Player, reader *minecraft.Reader) { 11 | player.Send(status.Response{ 12 | Response: status.ServerListResponse{ 13 | Version: status.ServerListVersion{ 14 | Name: "1.15.2", 15 | Protocol: 578, 16 | }, 17 | Players: status.ServerListPlayers{ 18 | Max: math.MaxInt32, 19 | Online: int(game.GetPlayerCount()), 20 | }, 21 | Description: minecraft.Chat{ 22 | Text: "Go Minecraft Server!", 23 | Italic: true, 24 | Color: "red", 25 | }, 26 | }, 27 | }) 28 | } 29 | 30 | func HandlePing(player *game.Player, reader *minecraft.Reader) { 31 | payload := reader.ReadLong() 32 | player.Send(status.Pong{ 33 | Payload: payload, 34 | }) 35 | } 36 | --------------------------------------------------------------------------------