diff --git a/.gitignore b/.gitignore index bc4f570..647d0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ server.properties data.qfg /blockstates server-icon.png -/resources \ No newline at end of file +/resources +/server/session/atomicSlice.go +/server/session/a_test.go \ No newline at end of file diff --git a/go.mod b/go.mod index ac03362..9bfe96f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/zeppelinmc/zeppelin -go 1.22.5 +go 1.23 require ( github.com/fatih/color v1.17.0 diff --git a/main.go b/main.go index 3524dc4..e514ce1 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "runtime" + "runtime/debug" "runtime/pprof" "slices" "strconv" @@ -17,6 +18,7 @@ import ( "github.com/zeppelinmc/zeppelin/server/command" "github.com/zeppelinmc/zeppelin/server/world" "github.com/zeppelinmc/zeppelin/server/world/chunk/section" + "github.com/zeppelinmc/zeppelin/util" "github.com/zeppelinmc/zeppelin/util/console" "github.com/zeppelinmc/zeppelin/util/log" "golang.org/x/term" @@ -26,11 +28,19 @@ var timeStart = time.Now() func main() { log.Infolnf("Zeppelin 1.21 Minecraft server with %s on platform %s-%s", runtime.Version(), runtime.GOOS, runtime.GOARCH) - if !loadStates() { return } + max, ok := console.GetFlag("xmem") + if ok { + m, err := util.ParseSizeUnit(max) + if err == nil { + debug.SetMemoryLimit(int64(m)) + log.Infolnf("Memory usage is limited to %s", util.FormatSizeUnit(m)) + } + } + if slices.Index(os.Args, "--cpuprof") != -1 { if f, err := os.Create("zeppelin-cpu-profile"); err == nil { pprof.StartCPUProfile(f) diff --git a/protocol/nbt/qnbt/primitive.go b/protocol/nbt/qnbt/primitive.go index 32efa4b..dcb3cb1 100644 --- a/protocol/nbt/qnbt/primitive.go +++ b/protocol/nbt/qnbt/primitive.go @@ -247,10 +247,6 @@ func (d *Decoder) decodeIntArray(len int, ptr unsafe.Pointer) error { return err } - if !native.SystemBigEndian { - native.Convert32(unsafe.Slice((*byte)(ptr), 4)) - } - ptr = unsafe.Add(ptr, 4) } diff --git a/protocol/nbt/qnbt/qnbt_test.go b/protocol/nbt/qnbt/qnbt_test.go index d64afca..dc46f5a 100644 --- a/protocol/nbt/qnbt/qnbt_test.go +++ b/protocol/nbt/qnbt/qnbt_test.go @@ -103,6 +103,9 @@ type chunk struct { } func BenchmarkXxx(b *testing.B) { + f1, _ := os.Create("testdata/cpu.prof") + pprof.StartCPUProfile(f1) + for i := 0; i < b.N; i++ { Unmarshal(chunkData, &chunk{}) } @@ -110,4 +113,7 @@ func BenchmarkXxx(b *testing.B) { f, _ := os.Create("testdata/mem.prof") pprof.Lookup("allocs").WriteTo(f, 0) f.Close() + + pprof.StopCPUProfile() + f1.Close() } diff --git a/protocol/nbt/qnbt/struct.go b/protocol/nbt/qnbt/struct.go index 172c4cc..df546ac 100644 --- a/protocol/nbt/qnbt/struct.go +++ b/protocol/nbt/qnbt/struct.go @@ -18,11 +18,11 @@ var structs = sync.Pool{ func newStruct(t *structType, ptr unsafe.Pointer) *struct_t { s := structs.Get().(*struct_t) + clear(s.names) + s.t = t s.ptr = ptr - clear(s.names) - for i, f := range t.fields { if !f.name.exported() { continue @@ -68,6 +68,7 @@ type struct_t struct { func (s *struct_t) field(n string) (f *structField, ptr unsafe.Pointer, ok bool) { i, ok := s.names[n] + if !ok { return nil, nil, ok } diff --git a/protocol/nbt/qnbt/testdata/cpu.prof b/protocol/nbt/qnbt/testdata/cpu.prof new file mode 100644 index 0000000..a3fa826 Binary files /dev/null and b/protocol/nbt/qnbt/testdata/cpu.prof differ diff --git a/protocol/nbt/qnbt/testdata/mem.prof b/protocol/nbt/qnbt/testdata/mem.prof index 3544a19..27e9d63 100644 Binary files a/protocol/nbt/qnbt/testdata/mem.prof and b/protocol/nbt/qnbt/testdata/mem.prof differ diff --git a/protocol/net/conn.go b/protocol/net/conn.go index c49de99..39a5261 100644 --- a/protocol/net/conn.go +++ b/protocol/net/conn.go @@ -23,6 +23,12 @@ import ( "github.com/google/uuid" ) +var PacketEncodeInterceptor func(c *Conn, pk packet.Encodeable) (stop bool) +var PacketDecodeInterceptor func(c *Conn, pk packet.Decodeable) (stop bool) + +var PacketWriteInterceptor func(c *Conn, pk *bytes.Buffer) (stop bool) +var PacketReadInterceptor func(c *Conn, pk *bytes.Reader) (stop bool) + const ( clientVeryOldMsg = "Your client is WAYYYYYY too old!!! this server supports MC 1.21" clientTooOldMsg = "Your client is too old! this server supports MC 1.21" @@ -87,6 +93,12 @@ var pkpool = sync.Pool{ } func (conn *Conn) WritePacket(pk packet.Encodeable) error { + if PacketEncodeInterceptor != nil { + if PacketEncodeInterceptor(conn, pk) { + return nil + } + } + conn.write_mu.Lock() defer conn.write_mu.Unlock() @@ -103,6 +115,12 @@ func (conn *Conn) WritePacket(pk packet.Encodeable) error { return err } + if PacketWriteInterceptor != nil { + if PacketWriteInterceptor(conn, packetBuf) { + return nil + } + } + if conn.listener.cfg.CompressionThreshold < 0 || !conn.compressionSet { // no compression if err := io.WriteVarInt(conn, int32(packetBuf.Len())); err != nil { return err @@ -202,7 +220,7 @@ func (conn *Conn) ReadPacket() (packet.Decodeable, error) { if _, err := conn.Read(packet); err != nil { return nil, err } - id, data, err := io.ReadVarInt(packet) + id, data, err := io.VarInt(packet) if err != nil { return nil, err } @@ -238,7 +256,7 @@ func (conn *Conn) ReadPacket() (packet.Decodeable, error) { return nil, err } - id, data, err := io.ReadVarInt(packet) + id, data, err := io.VarInt(packet) if err != nil { return nil, err } @@ -246,7 +264,15 @@ func (conn *Conn) ReadPacket() (packet.Decodeable, error) { packet = data length = int32(len(data)) - rd = io.NewReader(bytes.NewReader(packet), int(length)) + r := bytes.NewReader(packet) + + if PacketReadInterceptor != nil { + if PacketReadInterceptor(conn, r) { + return nil, fmt.Errorf("stopped by interceptor") + } + } + + rd = io.NewReader(r, int(length)) } } else { //packet is compressed length = dataLength @@ -264,7 +290,7 @@ func (conn *Conn) ReadPacket() (packet.Decodeable, error) { return nil, err } - id, data, err := io.ReadVarInt(uncompressedPacket) + id, data, err := io.VarInt(uncompressedPacket) if err != nil { return nil, err } @@ -272,12 +298,21 @@ func (conn *Conn) ReadPacket() (packet.Decodeable, error) { uncompressedPacket = data length = int32(len(data)) - rd = io.NewReader(bytes.NewReader(uncompressedPacket), int(length)) + r := bytes.NewReader(uncompressedPacket) + + if PacketReadInterceptor != nil { + if PacketReadInterceptor(conn, r) { + return nil, fmt.Errorf("stopped by interceptor") + } + } + + rd = io.NewReader(r, int(length)) } } var pk packet.Decodeable pc, ok := ServerboundPool[conn.state.Load()][packetId] + if !ok { return packet.UnknownPacket{ Id: packetId, @@ -286,6 +321,13 @@ func (conn *Conn) ReadPacket() (packet.Decodeable, error) { }, nil } else { pk = pc() + + if PacketDecodeInterceptor != nil { + if PacketDecodeInterceptor(conn, pk) { + return nil, fmt.Errorf("stopped by interceptor") + } + } + err := pk.Decode(rd) return pk, err } diff --git a/protocol/net/io/io.go b/protocol/net/io/io.go index 7bf9e04..29e2171 100644 --- a/protocol/net/io/io.go +++ b/protocol/net/io/io.go @@ -3,6 +3,7 @@ package io import ( "fmt" "io" + "unsafe" ) func AppendByte(data []byte, b int8) []byte { @@ -52,7 +53,7 @@ func WriteVarInt(w io.Writer, value int32) error { return err } -func ReadVarInt(data []byte) (int32, []byte, error) { +func VarInt(data []byte) (int32, []byte, error) { var ( position int currentByte byte @@ -83,6 +84,37 @@ func ReadVarInt(data []byte) (int32, []byte, error) { return value, data, nil } +func ReadVarInt(r io.Reader) (int32, error) { + var ( + position int32 + currentByte byte + CONTINUE_BIT byte = 128 + SEGMENT_BITS byte = 127 + + value int32 + ) + + for { + if _, err := r.Read(unsafe.Slice(¤tByte, 1)); err != nil { + return value, err + } + + value |= int32((currentByte & SEGMENT_BITS)) << position + + if (currentByte & CONTINUE_BIT) == 0 { + break + } + + position += 7 + + if position >= 32 { + return value, fmt.Errorf("VarInt is too big") + } + } + + return value, nil +} + func AppendVarLong(data []byte, value int64) []byte { var ( CONTINUE_BIT int64 = 128 diff --git a/server/command/builder.go b/server/command/builder.go index 0371580..04670f5 100644 --- a/server/command/builder.go +++ b/server/command/builder.go @@ -56,6 +56,12 @@ const ( UUID ) +const ( + StringSingleWord = iota + StringQuotablePhrase + StringGreedyPhrase +) + type Node struct { play.Node children []Node @@ -89,3 +95,40 @@ func NewBoolArgument(name string, nodes ...Node) Node { children: nodes, } } + +func NewIntegerArgument(name string, min, max *int32, nodes ...Node) Node { + flags := byte(0) + + var m, x int32 + + if min != nil { + flags &= 0x01 + m = *min + } + if max != nil { + flags &= 0x02 + x = *max + } + + return Node{ + Node: play.Node{ + Flags: play.NodeArgument, + Name: name, + ParserId: Integer, + Properties: []any{flags, m, x}, + }, + children: nodes, + } +} + +func NewStringArgument(name string, typ int32, nodes ...Node) Node { + return Node{ + Node: play.Node{ + Flags: play.NodeArgument, + Name: name, + ParserId: Integer, + Properties: []any{typ}, + }, + children: nodes, + } +} diff --git a/server/player/atomic.go b/server/player/atomic.go new file mode 100644 index 0000000..6fcbab2 --- /dev/null +++ b/server/player/atomic.go @@ -0,0 +1,44 @@ +package player + +import ( + "sync/atomic" + "unsafe" +) + +func i64f(i int64) float64 { + return *(*float64)(unsafe.Pointer(&i)) +} +func f64i(f float64) int64 { + return *(*int64)(unsafe.Pointer(&f)) +} + +func i32f(i int32) float32 { + return *(*float32)(unsafe.Pointer(&i)) +} +func f32i(f float32) int32 { + return *(*int32)(unsafe.Pointer(&f)) +} + +func atomicFloat64(f float64) *atomic.Int64 { + var v *atomic.Int64 + v.Store(f64i(f)) + + return v +} +func atomicFloat32(f float32) *atomic.Int32 { + return atomicInt32(f32i(f)) +} + +func atomicInt32(i int32) *atomic.Int32 { + var v *atomic.Int32 + v.Store(i) + + return v +} + +func atomicBool(b bool) *atomic.Bool { + var v *atomic.Bool + v.Store(b) + + return v +} diff --git a/server/player/player.go b/server/player/player.go index 5dac3b9..761769e 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -4,6 +4,7 @@ import ( "maps" "slices" "sync" + a "sync/atomic" "github.com/google/uuid" "github.com/zeppelinmc/zeppelin/protocol/net/metadata" @@ -19,16 +20,16 @@ var _ entity.LivingEntity = (*Player)(nil) type Player struct { entityId int32 - data level.PlayerData - x, y, z atomic.AtomicValue[float64] - vx, vy, vz atomic.AtomicValue[float64] - yaw, pitch atomic.AtomicValue[float32] - onGround atomic.AtomicValue[bool] + data level.Player + x, y, z, + vx, vy, vz a.Int64 + yaw, pitch a.Int32 + onGround a.Bool - health atomic.AtomicValue[float32] - food atomic.AtomicValue[int32] - foodExhaustion atomic.AtomicValue[float32] - foodSaturation atomic.AtomicValue[float32] + health, + food, + foodExhaustion, + foodSaturation a.Int32 abilities atomic.AtomicValue[level.PlayerAbilities] @@ -36,7 +37,7 @@ type Player struct { gameMode atomic.AtomicValue[level.GameMode] - selectedItemSlot atomic.AtomicValue[int32] + selectedItemSlot a.Int32 recipeBook atomic.AtomicValue[level.RecipeBook] @@ -50,7 +51,7 @@ type Player struct { } // looks up a player in the cache or creates one if not found -func (mgr *PlayerManager) New(data level.PlayerData) *Player { +func (mgr *PlayerManager) New(data level.Player) *Player { if p, ok := mgr.lookup(data.UUID.UUID()); ok { return p } @@ -82,18 +83,18 @@ func (mgr *PlayerManager) New(data level.PlayerData) *Player { metadata.PlayerMainHandIndex: metadata.Byte(1), }, - x: atomic.Value(data.Pos[0]), - y: atomic.Value(data.Pos[1]), - z: atomic.Value(data.Pos[2]), + x: *atomicFloat64(data.Pos[0]), + y: *atomicFloat64(data.Pos[1]), + z: *atomicFloat64(data.Pos[2]), - vx: atomic.Value(data.Motion[0]), - vy: atomic.Value(data.Motion[1]), - vz: atomic.Value(data.Motion[2]), + vx: *atomicFloat64(data.Motion[0]), + vy: *atomicFloat64(data.Motion[1]), + vz: *atomicFloat64(data.Motion[2]), - yaw: atomic.Value(data.Rotation[0]), - pitch: atomic.Value(data.Rotation[1]), + yaw: *atomicFloat32(data.Rotation[0]), + pitch: *atomicFloat32(data.Rotation[1]), - onGround: atomic.Value(data.OnGround), + onGround: *atomicBool(data.OnGround), dimension: atomic.Value(data.Dimension), @@ -101,12 +102,12 @@ func (mgr *PlayerManager) New(data level.PlayerData) *Player { recipeBook: atomic.Value(data.RecipeBook), - selectedItemSlot: atomic.Value(data.SelectedItemSlot), + selectedItemSlot: *atomicInt32(data.SelectedItemSlot), - health: atomic.Value(data.Health), - food: atomic.Value(data.FoodLevel), - foodExhaustion: atomic.Value(data.FoodExhaustionLevel), - foodSaturation: atomic.Value(data.FoodSaturationLevel), + health: *atomicFloat32(data.Health), + food: *atomicInt32(data.FoodLevel), + foodExhaustion: *atomicFloat32(data.FoodExhaustionLevel), + foodSaturation: *atomicFloat32(data.FoodSaturationLevel), abilities: atomic.Value(data.Abilities), @@ -130,40 +131,40 @@ func (p *Player) UUID() uuid.UUID { } func (p *Player) Position() (x, y, z float64) { - return p.x.Get(), p.y.Get(), p.z.Get() + return i64f(p.x.Load()), i64f(p.y.Load()), i64f(p.z.Load()) } func (p *Player) Rotation() (yaw, pitch float32) { - return p.yaw.Get(), p.pitch.Get() + return i32f(p.yaw.Load()), i32f(p.pitch.Load()) } func (p *Player) OnGround() bool { - return p.onGround.Get() + return p.onGround.Load() } func (p *Player) SetPosition(x, y, z float64) { - p.x.Set(x) - p.y.Set(y) - p.z.Set(z) + p.x.Store(f64i(x)) + p.y.Store(f64i(y)) + p.z.Store(f64i(z)) } func (p *Player) SetRotation(yaw, pitch float32) { - p.yaw.Set(yaw) - p.pitch.Set(pitch) + p.yaw.Store(f32i(yaw)) + p.pitch.Store(f32i(pitch)) } func (p *Player) SetOnGround(val bool) { - p.onGround.Set(val) + p.onGround.Store(val) } func (p *Player) Motion() (x, y, z float64) { - return p.vx.Get(), p.vy.Get(), p.vz.Get() + return i64f(p.vx.Load()), i64f(p.vy.Load()), i64f(p.vz.Load()) } func (p *Player) SetMotion(x, y, z float64) { - p.vx.Set(x) - p.vy.Set(y) - p.vz.Set(z) + p.vx.Store(f64i(x)) + p.vy.Store(f64i(y)) + p.vz.Store(f64i(z)) } func (p *Player) EntityId() int32 { @@ -212,35 +213,35 @@ func (p *Player) SetDimension(dim string) { } func (p *Player) Health() float32 { - return p.health.Get() + return i32f(p.health.Load()) } func (p *Player) SetHealth(h float32) { - p.health.Set(h) + p.health.Store(f32i(h)) } func (p *Player) Food() int32 { - return p.food.Get() + return p.food.Load() } func (p *Player) SetFood(f int32) { - p.food.Set(f) + p.food.Store(f) } func (p *Player) FoodSaturation() float32 { - return p.foodSaturation.Get() + return i32f(p.foodSaturation.Load()) } func (p *Player) SetFoodSaturation(fs float32) { - p.foodSaturation.Set(fs) + p.foodSaturation.Store(f32i(fs)) } func (p *Player) FoodExhaustion() float32 { - return p.foodExhaustion.Get() + return i32f(p.foodExhaustion.Load()) } func (p *Player) SetFoodExhaustion(fh float32) { - p.foodExhaustion.Set(fh) + p.foodExhaustion.Store(f32i(fh)) } func (p *Player) Abilities() level.PlayerAbilities { @@ -310,7 +311,7 @@ func (p *Player) Inventory() *container.Container { // if negative, returns 0 and if over 8, returns 8 func (p *Player) SelectedItemSlot() int32 { - slot := p.selectedItemSlot.Get() + slot := p.selectedItemSlot.Load() if slot < 0 { slot = 0 } @@ -328,31 +329,32 @@ func (p *Player) SetSelectedItemSlot(slot int32) { if slot > 8 { slot = 8 } - p.selectedItemSlot.Set(slot) + p.selectedItemSlot.Store(slot) } func (p *Player) sync() { x, y, z := p.Position() yaw, pitch := p.Rotation() + vx, vy, vz := p.Motion() p.data.Abilities = p.abilities.Get() p.data.Pos = [3]float64{x, y, z} p.data.Rotation = [2]float32{yaw, pitch} - p.data.OnGround = p.onGround.Get() + p.data.OnGround = p.onGround.Load() p.data.Dimension = p.dimension.Get() p.data.Inventory = *p.inventory p.data.RecipeBook = p.recipeBook.Get() + p.data.Motion = [3]float64{vx, vy, vz} + p.att_mu.RLock() p.data.Attributes = p.attributes p.att_mu.RUnlock() - p.data.Health = p.health.Get() - p.data.FoodLevel = p.food.Get() - p.data.FoodExhaustionLevel = p.foodExhaustion.Get() - p.data.FoodSaturationLevel = p.foodSaturation.Get() + p.data.Health = i32f(p.health.Load()) + p.data.FoodLevel = p.food.Load() + p.data.FoodExhaustionLevel = i32f(p.foodExhaustion.Load()) + p.data.FoodSaturationLevel = i32f(p.foodSaturation.Load()) p.data.PlayerGameType = p.gameMode.Get() - p.data.SelectedItemSlot = p.selectedItemSlot.Get() - - //TODO motion(velocity), xp, etc + p.data.SelectedItemSlot = p.selectedItemSlot.Load() } diff --git a/server/plugin.go b/server/plugin.go index d50214a..3e203d2 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -9,6 +9,8 @@ import ( ) type Plugin struct { + basePluginsPath string + srv *Server Identifier string @@ -16,6 +18,15 @@ type Plugin struct { Unload func(*Plugin) } +func (p Plugin) FS() fs.FS { + return os.DirFS(p.Dir()) +} + +// Dir returns the base directory for the plugin (plugins/) +func (p Plugin) Dir() string { + return p.basePluginsPath + "/" + p.Identifier +} + func (p Plugin) Server() *Server { return p.srv } @@ -48,6 +59,7 @@ func (srv *Server) loadPlugin(name string) { log.Errorlnf("Invalid plugin export for %s", name) return } + plugin.basePluginsPath = "plugins" plugin.srv = srv plugin.OnLoad(plugin) } diff --git a/server/server.go b/server/server.go index fe58f21..1b9e578 100644 --- a/server/server.go +++ b/server/server.go @@ -130,7 +130,7 @@ func (srv *Server) Properties() properties.ServerProperties { } func (srv *Server) Start(ts time.Time) { - if slices.Index(os.Args, "--no-plugins") != -1 { + if slices.Index(os.Args, "--no-plugins") == -1 { if runtime.GOOS == "darwin" || runtime.GOOS == "linux" || runtime.GOOS == "freebsd" { log.Infoln("Loading plugins") srv.loadPlugins() diff --git a/server/session/std/handler.go b/server/session/std/handler.go index f6a0b65..7fe76d9 100644 --- a/server/session/std/handler.go +++ b/server/session/std/handler.go @@ -12,8 +12,8 @@ import ( "github.com/zeppelinmc/zeppelin/util/log" ) -var PacketReadInterceptor func(s *StandardSession, pk packet.Decodeable, stop *bool) -var PacketWriteInterceptor func(s *StandardSession, pk packet.Encodeable, stop *bool) +var PacketReadInterceptor func(s *StandardSession, pk packet.Decodeable) bool +var PacketWriteInterceptor func(s *StandardSession, pk packet.Encodeable) bool type handler func(*StandardSession, packet.Decodeable) @@ -28,7 +28,7 @@ func (session *StandardSession) inConfiguration() bool { func (session *StandardSession) readIntercept(pk packet.Decodeable) (stop bool) { if PacketReadInterceptor != nil { - PacketReadInterceptor(session, pk, &stop) + return PacketReadInterceptor(session, pk) } return @@ -36,7 +36,6 @@ func (session *StandardSession) readIntercept(pk packet.Decodeable) (stop bool) func (session *StandardSession) handlePackets() { keepAlive := time.NewTicker(time.Second * 20) - //ticker := session.tick.New() for { select { case <-keepAlive.C: @@ -72,12 +71,12 @@ func (session *StandardSession) handlePackets() { session.broadcast.PlayerInfoUpdateSession(session) case *configuration.ServerboundPluginMessage: if pk.Channel == "minecraft:brand" { - _, data, _ := io.ReadVarInt(pk.Data) + _, data, _ := io.VarInt(pk.Data) session.clientName = string(data) } case *play.ServerboundPluginMessage: if pk.Channel == "minecraft:brand" { - _, data, _ := io.ReadVarInt(pk.Data) + _, data, _ := io.VarInt(pk.Data) session.clientName = string(data) } case *configuration.AcknowledgeFinishConfiguration: diff --git a/server/session/std/handler/movement.go b/server/session/std/handler/movement.go index e4ecc3e..cda4e85 100644 --- a/server/session/std/handler/movement.go +++ b/server/session/std/handler/movement.go @@ -1,13 +1,10 @@ package handler import ( - "math" - "github.com/zeppelinmc/zeppelin/protocol/net" "github.com/zeppelinmc/zeppelin/protocol/net/packet" "github.com/zeppelinmc/zeppelin/protocol/net/packet/play" "github.com/zeppelinmc/zeppelin/server/session/std" - "github.com/zeppelinmc/zeppelin/util/log" ) func init() { @@ -17,71 +14,22 @@ func init() { std.RegisterHandler(net.PlayState, play.PacketIdSetPlayerOnGround, handleMovement) } -func chunkPos(x, z float64) (cx, cz int32) { - return int32(math.Floor(x / 16)), int32(math.Floor(z / 16)) -} - func handleMovement(s *std.StandardSession, p packet.Decodeable) { if s.AwaitingTeleportAcknowledgement.Get() { return } switch pk := p.(type) { case *play.SetPlayerPosition: - oldX, oldY, oldZ := s.Player().Position() - oldChunkPosX, oldChunkPosZ := chunkPos(oldX, oldZ) - newChunkPosX, newChunkPosZ := chunkPos(pk.X, pk.Z) - - if oldChunkPosX != newChunkPosX || oldChunkPosZ != newChunkPosZ { - s.Conn().WritePacket(&play.SetCenterChunk{ChunkX: newChunkPosX, ChunkZ: newChunkPosZ}) - s.ChunkLoadWorker.SendChunksRadius(newChunkPosX, newChunkPosZ) - } - - yaw, pitch := s.Player().Rotation() - - distance := math.Sqrt((pk.X-oldX)*(pk.X-oldX) + (pk.Y-oldY)*(pk.Y-oldY) + (pk.Z-oldZ)*(pk.Z-oldZ)) - - if distance > 100 { - s.SynchronizePosition(oldX, oldY, oldZ, yaw, pitch) - log.Infof("%s moved too quickly! (%f %f %f)\n", s.Username(), pk.X-oldX, pk.Y-oldY, pk.Z-oldZ) - return - } - - s.Broadcast().BroadcastPlayerMovement(s, pk.X, pk.Y, pk.Z, yaw, pitch) - - s.Player().SetPosition(pk.X, pk.Y, pk.Z) - s.Player().SetOnGround(pk.OnGround) + s.Input.SetPosition(pk.X, pk.Y, pk.Z) + s.Input.SetOnGround(pk.OnGround) case *play.SetPlayerPositionAndRotation: - oldX, oldY, oldZ := s.Player().Position() - oldChunkPosX, oldChunkPosZ := chunkPos(oldX, oldZ) - newChunkPosX, newChunkPosZ := chunkPos(pk.X, pk.Z) - - if oldChunkPosX != newChunkPosX || oldChunkPosZ != newChunkPosZ { - s.Conn().WritePacket(&play.SetCenterChunk{ChunkX: newChunkPosX, ChunkZ: newChunkPosZ}) - s.ChunkLoadWorker.SendChunksRadius(newChunkPosX, newChunkPosZ) - } - - distance := math.Sqrt((pk.X-oldX)*(pk.X-oldX) + (pk.Y-oldY)*(pk.Y-oldY) + (pk.Z-oldZ)*(pk.Z-oldZ)) - - if distance > 100 { - s.SynchronizePosition(oldX, oldY, oldZ, pk.Yaw, pk.Pitch) - log.Infof("%s moved too quickly! (%f %f %f)\n", s.Username(), pk.X-oldX, pk.Y-oldY, pk.Z-oldZ) - return - } - - s.Broadcast().BroadcastPlayerMovement(s, pk.X, pk.Y, pk.Z, pk.Yaw, pk.Pitch) - - s.Player().SetPosition(pk.X, pk.Y, pk.Z) - s.Player().SetRotation(pk.Yaw, pk.Pitch) - s.Player().SetOnGround(pk.OnGround) + s.Input.SetPosition(pk.X, pk.Y, pk.Z) + s.Input.SetRotation(pk.Yaw, pk.Pitch) + s.Input.SetOnGround(pk.OnGround) case *play.SetPlayerRotation: - // you can never rotate too much :) - x, y, z := s.Player().Position() - - s.Broadcast().BroadcastPlayerMovement(s, x, y, z, pk.Yaw, pk.Pitch) - - s.Player().SetRotation(pk.Yaw, pk.Pitch) - s.Player().SetOnGround(pk.OnGround) + s.Input.SetRotation(pk.Yaw, pk.Pitch) + s.Input.SetOnGround(pk.OnGround) case *play.SetPlayerOnGround: - s.Player().SetOnGround(pk.OnGround) + s.Input.SetOnGround(pk.OnGround) } } diff --git a/server/session/std/handler/useItemOn.go b/server/session/std/handler/useItemOn.go index 57a006f..b3819d9 100644 --- a/server/session/std/handler/useItemOn.go +++ b/server/session/std/handler/useItemOn.go @@ -96,7 +96,7 @@ func handleUseItemOn(s *std.StandardSession, pk packet.Decodeable) { blockSet = blockSet.New(map[string]string{"axis": axis, "facing": facing}) - s.Conn().WritePacket(&play.AcknowledgeBlockChange{SequenceId: use.Sequence}) + s.WritePacket(&play.AcknowledgeBlockChange{SequenceId: use.Sequence}) pos := pos.New(blockX, blockY, blockZ) dimension.SetBlock(pos, blockSet, true) diff --git a/server/session/std/session.go b/server/session/std/session.go index f46a608..fa1ca84 100644 --- a/server/session/std/session.go +++ b/server/session/std/session.go @@ -46,6 +46,7 @@ type StandardSession struct { broadcast *session.Broadcast config properties.ServerProperties + Input Input ChunkLoadWorker *ChunkLoadWorker conn *net.Conn @@ -121,6 +122,10 @@ func New( } s.ChunkLoadWorker = NewChunkLoadWorker(s) + s.Input.SetPosition(s.player.Position()) + s.Input.SetRotation(s.player.Rotation()) + s.Input.SetOnGround(s.player.OnGround()) + return s } @@ -130,9 +135,7 @@ func (session *StandardSession) CommandManager() *command.Manager { func (session *StandardSession) WritePacket(pk packet.Encodeable) error { if PacketWriteInterceptor != nil { - var stop bool - PacketWriteInterceptor(session, pk, &stop) - if stop { + if PacketWriteInterceptor(session, pk) { return nil } } @@ -388,6 +391,7 @@ func (session *StandardSession) login() error { } session.broadcast.SpawnPlayer(session) + session.createTicker() return nil } diff --git a/server/session/std/tick.go b/server/session/std/tick.go new file mode 100644 index 0000000..4520c06 --- /dev/null +++ b/server/session/std/tick.go @@ -0,0 +1,116 @@ +package std + +import ( + "math" + "sync/atomic" + + "github.com/zeppelinmc/zeppelin/protocol/net/packet/play" + "github.com/zeppelinmc/zeppelin/util/log" +) + +func chunkPos(x, z float64) (cx, cz int32) { + return int32(math.Floor(x / 16)), int32(math.Floor(z / 16)) +} + +type Input struct { + x, y, z atomic.Uint64 + yaw, pitch atomic.Uint32 + onGround atomic.Bool +} + +func (i *Input) SetPosition(x, y, z float64) { + i.x.Store(math.Float64bits(x)) + i.y.Store(math.Float64bits(y)) + i.z.Store(math.Float64bits(z)) +} + +func (i *Input) SetRotation(yaw, pitch float32) { + i.yaw.Store(math.Float32bits(yaw)) + i.pitch.Store(math.Float32bits(pitch)) +} + +func (i *Input) SetOnGround(b bool) { + i.onGround.Store(b) +} + +func (i *Input) Position() (x, y, z float64) { + return math.Float64frombits(i.x.Load()), math.Float64frombits(i.y.Load()), math.Float64frombits(i.z.Load()) +} + +func (i *Input) Rotation() (yaw, pitch float32) { + return math.Float32frombits(i.yaw.Load()), math.Float32frombits(i.pitch.Load()) +} + +func (i *Input) OnGround() bool { + return i.onGround.Load() +} + +func (session *StandardSession) createTicker() { + ticker := session.tick.New() + go func() { + for range ticker.C { + session.processInput() + } + }() +} + +func (session *StandardSession) processInput() { + x, y, z, yaw, pitch, onGround := session.input() + oldX, oldY, oldZ, oldYaw, oldPitch, oldOnGround := session.state() + + posC, rotC, onC := session.inputUpdated(x, y, z, yaw, pitch, onGround, oldX, oldY, oldZ, oldYaw, oldPitch, oldOnGround) + if !posC && !rotC && !onC { + return + } + + if posC { + oldChunkPosX, oldChunkPosZ := chunkPos(oldX, oldZ) + newChunkPosX, newChunkPosZ := chunkPos(x, z) + + if oldChunkPosX != newChunkPosX || oldChunkPosZ != newChunkPosZ { + session.WritePacket(&play.SetCenterChunk{ChunkX: newChunkPosX, ChunkZ: newChunkPosZ}) + session.ChunkLoadWorker.SendChunksRadius(newChunkPosX, newChunkPosZ) + } + + distance := math.Sqrt((x-oldX)*(x-oldX) + (y-oldY)*(y-oldY) + (z-oldZ)*(z-oldZ)) + + if distance > 100 { + session.SynchronizePosition(oldX, oldY, oldZ, oldYaw, oldPitch) + log.Infof("%s moved too quickly! (%f %f %f)\n", session.Username(), x-oldX, y-oldY, z-oldZ) + return + } + defer session.player.SetPosition(x, y, z) + } + if rotC { + defer session.player.SetRotation(yaw, pitch) + } + if onC { + defer session.player.SetOnGround(onGround) + } + + session.broadcast.BroadcastPlayerMovement(session, x, y, z, yaw, pitch) +} + +func (session *StandardSession) inputUpdated( + x, y, z float64, yaw, pitch float32, onGround bool, + oldX, oldY, oldZ float64, oldYaw, oldPitch float32, oldOnGround bool, +) (posChanged, rotChanged, onGroundChanged bool) { + + return x != oldX || y != oldY || z != oldZ, yaw != oldYaw || pitch != oldPitch, onGround != oldOnGround +} + +func (session *StandardSession) input() (x, y, z float64, yaw, pitch float32, onGround bool) { + x, y, z = session.Input.Position() + yaw, pitch = session.Input.Rotation() + onGround = session.Input.OnGround() + + return +} + +func (session *StandardSession) state() (oldX, oldY, oldZ float64, oldYaw, oldPitch float32, oldOnGround bool) { + oldX, oldY, oldZ = session.player.Position() + oldYaw, oldPitch = session.player.Rotation() + oldOnGround = session.player.OnGround() + + return +} diff --git a/server/world/level/playerdata.go b/server/world/level/playerdata.go index c1dbdd6..bbb5672 100644 --- a/server/world/level/playerdata.go +++ b/server/world/level/playerdata.go @@ -15,7 +15,7 @@ import ( datauuid "github.com/zeppelinmc/zeppelin/server/world/level/uuid" ) -type PlayerData struct { +type Player struct { // the path of this playerdata file, not a field in the nbt path string `nbt:"-"` // the base path of the world @@ -88,7 +88,7 @@ type PlayerData struct { } `nbt:"warden_spawn_tracker"` } -func (data *PlayerData) Save() error { +func (data *Player) Save() error { os.MkdirAll(data.basePath+"/playerdata", 0755) os.Rename(data.path, data.path+"_old") file, err := os.Create(data.path) @@ -107,8 +107,8 @@ func (data *PlayerData) Save() error { return file.Close() } -func (w *Level) PlayerData(uuid string) (PlayerData, error) { - var playerData PlayerData +func (w *Level) PlayerData(uuid string) (Player, error) { + var playerData Player path := fmt.Sprintf("%s/playerdata/%s.dat", w.basePath, uuid) file, err := os.Open(path) @@ -131,8 +131,8 @@ func (w *Level) PlayerData(uuid string) (PlayerData, error) { return playerData, err } -func (w *Level) NewPlayerData(uuid uuid.UUID) PlayerData { - return PlayerData{ +func (w *Level) NewPlayerData(uuid uuid.UUID) Player { + return Player{ path: fmt.Sprintf("%s/playerdata/%s.dat", w.basePath, uuid), basePath: w.basePath, Pos: [3]float64{float64(w.Data.SpawnX), float64(w.Data.SpawnY), float64(w.Data.SpawnZ)}, diff --git a/util/console/console.go b/util/console/console.go index 5c5ffec..bce4c85 100644 --- a/util/console/console.go +++ b/util/console/console.go @@ -10,6 +10,19 @@ import ( "github.com/zeppelinmc/zeppelin/server" ) +func GetFlag(name string) (string, bool) { + name = "--" + name + "=" + for _, a := range os.Args { + if i := strings.Index(a, name); i == 0 { + if len(name)+i < len(a) { + return a[len(name)+i:], true + } + } + } + + return "", false +} + func StartConsole(srv *server.Server) { var line string var scanner = bufio.NewScanner(os.Stdin) diff --git a/util/unit.go b/util/unit.go new file mode 100644 index 0000000..6392227 --- /dev/null +++ b/util/unit.go @@ -0,0 +1,74 @@ +package util + +import ( + "fmt" + "strconv" + "strings" +) + +var sizeUnits = map[string]uint64{ + "kb": 1000, // kilobyte (decimal) + "kib": 1024, // kibibyte (binary) + "mb": 1000 * 1000, // megabyte (decimal) + "mib": 1024 * 1024, // mebibyte (binary) + "gb": 1000 * 1000 * 1000, // gigabyte (decimal) + "gib": 1024 * 1024 * 1024, // gibibyte (binary) + "tb": 1000 * 1000 * 1000 * 1000, // terabyte (decimal) + "tib": 1024 * 1024 * 1024 * 1024, // tebibyte (binary) +} + +// ParseSizeUnit parses a size string into a number of bytes. +func ParseSizeUnit(sizeStr string) (uint64, error) { + // Trim any surrounding whitespace and convert to lowercase. + sizeStr = strings.TrimSpace(strings.ToLower(sizeStr)) + + // Find the position where the letters start. + for unit := range sizeUnits { + if strings.HasSuffix(sizeStr, unit) { + // Extract the numeric part of the string. + numberStr := strings.TrimSuffix(sizeStr, unit) + number, err := strconv.ParseUint(numberStr, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid number format: %w", err) + } + + // Convert the number to bytes using the corresponding unit. + return number * sizeUnits[unit], nil + } + } + + number, err := strconv.ParseUint(sizeStr, 10, 64) + return number, err +} + +var sizeUnitsS = []struct { + Unit string + Factor uint64 +}{ + {"B", 1}, + {"KiB", 1024}, + {"MiB", 1024 * 1024}, + {"GiB", 1024 * 1024 * 1024}, + {"TiB", 1024 * 1024 * 1024 * 1024}, + {"PiB", 1024 * 1024 * 1024 * 1024 * 1024}, +} + +// FormatSizeUnit formats a number of bytes into a human-readable string. +func FormatSizeUnit(bytes uint64) string { + if bytes == 0 { + return "0 B" + } + + var result strings.Builder + for i := len(sizeUnitsS) - 1; i >= 0; i-- { + unit := sizeUnitsS[i] + if bytes >= unit.Factor { + value := float64(bytes) / float64(unit.Factor) + result.WriteString(fmt.Sprintf("%.2f %s", value, unit.Unit)) + return result.String() + } + } + + // This should never be reached since we handle all sizes + return "0 B" +}