From 474d6a229bc1a0324ffd5e22d02bd5188861494c Mon Sep 17 00:00:00 2001 From: Tnze Date: Fri, 27 May 2022 00:38:46 +0800 Subject: [PATCH] player and dimension loader with ecs system --- examples/frameworkServer/main.go | 73 ++--------- nbt/decode.go | 13 +- save/playerdata.go | 8 +- server/chat.go | 131 -------------------- server/{player.go => client.go} | 57 +++------ server/clientinfo/clientinfo.go | 54 +++++--- server/command/component.go | 12 +- server/dimension.go | 8 +- server/dimension/world.go | 32 ----- server/ecs/bitest.go | 58 +++++++++ server/ecs/dispatcher.go | 44 +++++++ server/ecs/ecs_test.go | 34 ++++++ server/ecs/storage.go | 78 ++++++++++++ server/ecs/system.go | 66 ++++++++++ server/ecs/system_test.go | 13 ++ server/ecs/world.go | 90 ++++++++++++++ server/entity.go | 4 + server/gameplay.go | 140 ++++++++++----------- server/internal/ecs/bitest.go | 39 ------ server/internal/ecs/ecs_test.go | 27 ---- server/internal/ecs/storage.go | 34 ------ server/internal/ecs/system.go | 49 -------- server/internal/ecs/world.go | 58 --------- server/keepalive.go | 76 ++++++------ server/ping.go | 10 +- server/player/player.go | 112 +++++++++++++++++ server/player/playerinfo.go | 163 +++++++++++++++++++++++++ server/player/pool.go | 37 ++++++ server/playerinfo.go | 148 ---------------------- server/playerlist.go | 30 ++--- server/{ => world}/Dimension.snbt | 0 server/{ => world}/DimensionCodec.snbt | 0 server/{dimension => world}/pool.go | 2 +- server/world/world.go | 51 ++++++++ 34 files changed, 956 insertions(+), 795 deletions(-) delete mode 100644 server/chat.go rename server/{player.go => client.go} (68%) delete mode 100644 server/dimension/world.go create mode 100644 server/ecs/bitest.go create mode 100644 server/ecs/dispatcher.go create mode 100644 server/ecs/ecs_test.go create mode 100644 server/ecs/storage.go create mode 100644 server/ecs/system.go create mode 100644 server/ecs/system_test.go create mode 100644 server/ecs/world.go create mode 100644 server/entity.go delete mode 100644 server/internal/ecs/bitest.go delete mode 100644 server/internal/ecs/ecs_test.go delete mode 100644 server/internal/ecs/storage.go delete mode 100644 server/internal/ecs/system.go delete mode 100644 server/internal/ecs/world.go create mode 100644 server/player/player.go create mode 100644 server/player/playerinfo.go create mode 100644 server/player/pool.go delete mode 100644 server/playerinfo.go rename server/{ => world}/Dimension.snbt (100%) rename server/{ => world}/DimensionCodec.snbt (100%) rename server/{dimension => world}/pool.go (98%) create mode 100644 server/world/world.go diff --git a/examples/frameworkServer/main.go b/examples/frameworkServer/main.go index ed3d7fc..52945f4 100644 --- a/examples/frameworkServer/main.go +++ b/examples/frameworkServer/main.go @@ -4,20 +4,16 @@ import ( "context" _ "embed" "flag" - "fmt" + "github.com/Tnze/go-mc/server/world" "image" _ "image/png" "log" "os" - "path/filepath" - "time" "github.com/Tnze/go-mc/chat" - "github.com/Tnze/go-mc/level" - "github.com/Tnze/go-mc/save" - "github.com/Tnze/go-mc/save/region" "github.com/Tnze/go-mc/server" "github.com/Tnze/go-mc/server/command" + "github.com/Tnze/go-mc/server/player" ) var motd = chat.Message{Text: "A Minecraft Server ", Extra: []chat.Message{{Text: "Powered by go-mc", Color: "yellow"}}} @@ -35,11 +31,7 @@ func main() { } keepAlive := server.NewKeepAlive() - playerInfo := server.NewPlayerInfo(time.Second, keepAlive) - defaultDimension, err := loadAllRegions(*regionPath) - if err != nil { - log.Fatalf("Load chunks fail: %v", err) - } + //playerInfo := player.NewPlayerInfo(keepAlive) commands := command.NewGraph() handleFunc := func(ctx context.Context, args []command.ParsedData) error { @@ -59,21 +51,21 @@ func main() { HandleFunc(handleFunc)). HandleFunc(handleFunc), ) - game := server.NewGame( - defaultDimension, playerList, - playerInfo, + //playerInfo, keepAlive, - server.NewGlobalChat(), commands, ) + world.NewDimensionManager(game) + player.SpawnSystem(game, "./save/testdata/playerdata") + player.PosAndRotSystem(game) go game.Run(context.Background()) s := server.Server{ ListPingHandler: serverInfo, LoginHandler: &server.MojangLoginHandler{ - OnlineMode: false, + OnlineMode: true, Threshold: 256, LoginChecker: playerList, }, @@ -100,52 +92,3 @@ func readIcon() image.Image { } return icon } - -func loadAllRegions(dir string) (*server.SimpleDim, error) { - mcafiles, err := filepath.Glob(filepath.Join(dir, "r.*.*.mca")) - if err != nil { - return nil, err - } - dim := server.NewSimpleDim(256) - for _, file := range mcafiles { - var rx, rz int - _, err := fmt.Sscanf(filepath.Base(file), "r.%d.%d.mca", &rx, &rz) - if err != nil { - return nil, err - } - err = loadAllChunks(dim, file, rx, rz) - if err != nil { - return nil, err - } - } - return dim, nil -} - -func loadAllChunks(dim *server.SimpleDim, file string, rx, rz int) error { - r, err := region.Open(file) - if err != nil { - return err - } - defer r.Close() - var c save.Chunk - for x := 0; x < 32; x++ { - for z := 0; z < 32; z++ { - if !r.ExistSector(x, z) { - continue - } - data, err := r.ReadSector(x, z) - if err != nil { - return err - } - if err := c.Load(data); err != nil { - return err - } - chunk, err := level.ChunkFromSave(&c) - if err != nil { - return err - } - dim.LoadChunk(level.ChunkPos{X: rx<<5 + x, Z: rz<<5 + z}, chunk) - } - } - return nil -} diff --git a/nbt/decode.go b/nbt/decode.go index 5f68704..297619c 100644 --- a/nbt/decode.go +++ b/nbt/decode.go @@ -253,13 +253,18 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error { vt := val.Type() //receiver must be []int or []int32 if vt.Kind() == reflect.Interface { vt = reflect.TypeOf([]int32{}) // pass - } else if vt.Kind() != reflect.Slice { + } else if vt.Kind() == reflect.Array && vt.Len() != int(aryLen) { + return errors.New("cannot parse TagIntArray to " + vt.String() + ", length not match") + } else if k := vt.Kind(); k != reflect.Slice && k != reflect.Array { return errors.New("cannot parse TagIntArray to " + vt.String() + ", it must be a slice") } else if tk := val.Type().Elem().Kind(); tk != reflect.Int && tk != reflect.Int32 { return errors.New("cannot parse TagIntArray to " + vt.String()) } - buf := reflect.MakeSlice(vt, int(aryLen), int(aryLen)) + buf := val + if vt.Kind() == reflect.Slice { + buf = reflect.MakeSlice(vt, int(aryLen), int(aryLen)) + } for i := 0; i < int(aryLen); i++ { value, err := d.readInt() if err != nil { @@ -267,7 +272,9 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error { } buf.Index(i).SetInt(int64(value)) } - val.Set(buf) + if vt.Kind() == reflect.Slice { + val.Set(buf) + } case TagLongArray: aryLen, err := d.readInt() diff --git a/save/playerdata.go b/save/playerdata.go index 99f7866..6f0553b 100644 --- a/save/playerdata.go +++ b/save/playerdata.go @@ -1,9 +1,7 @@ package save import ( - "encoding/binary" "github.com/Tnze/go-mc/nbt" - "github.com/google/uuid" "io" ) @@ -18,8 +16,7 @@ type PlayerData struct { FallFlying byte OnGround byte - UUID uuid.UUID `nbt:"-"` - UUIDLeast, UUIDMost int64 + UUID [4]int32 PlayerGameType int32 `nbt:"playerGameType"` Air int16 @@ -80,8 +77,5 @@ type Item struct { func ReadPlayerData(r io.Reader) (data PlayerData, err error) { _, err = nbt.NewDecoder(r).Decode(&data) - //parse UUID from two int64s - binary.BigEndian.PutUint64(data.UUID[:], uint64(data.UUIDMost)) - binary.BigEndian.PutUint64(data.UUID[8:], uint64(data.UUIDLeast)) return } diff --git a/server/chat.go b/server/chat.go deleted file mode 100644 index b3463bd..0000000 --- a/server/chat.go +++ /dev/null @@ -1,131 +0,0 @@ -package server - -import ( - "context" - - "github.com/Tnze/go-mc/chat" - "github.com/Tnze/go-mc/data/packetid" - "github.com/Tnze/go-mc/nbt" - pk "github.com/Tnze/go-mc/net/packet" - "github.com/google/uuid" -) - -type GlobalChat struct { - msg chan chatItem - join chan *Player - quit chan *Player - - players map[uuid.UUID]*Player -} - -type chatItem struct { - p *Player - text string -} - -func NewGlobalChat() *GlobalChat { - return &GlobalChat{ - msg: make(chan chatItem), - join: make(chan *Player), - quit: make(chan *Player), - players: make(map[uuid.UUID]*Player), - } -} - -func (g *GlobalChat) Init(game *Game) { - game.AddHandler(&PacketHandler{ - ID: packetid.ServerboundChat, - F: func(player *Player, packet Packet758) error { - var msg pk.String - if err := pk.Packet(packet).Scan(&msg); err != nil { - return err - } - text, _ := chat.TransCtrlSeq(string(msg), false) - g.msg <- chatItem{p: player, text: text} - return nil - }, - }) -} - -const ( - chatPosChat = iota - chatPosSystem - chatPosGameInfo -) - -func (g *GlobalChat) Run(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case item := <-g.msg: - g.broadcast(Packet758(pk.Marshal( - packetid.ClientboundChat, - item.toMessage(), - pk.Byte(chatPosChat), - pk.UUID(item.p.UUID), - ))) - case p := <-g.join: - g.broadcast(Packet758(pk.Marshal( - packetid.ClientboundChat, - chat.TranslateMsg("multiplayer.player.joined", chat.Text(p.Name)).SetColor(chat.Yellow), - pk.Byte(chatPosSystem), - pk.UUID(uuid.Nil), - ))) - g.players[p.UUID] = p - case p := <-g.quit: - g.broadcast(Packet758(pk.Marshal( - packetid.ClientboundChat, - chat.TranslateMsg("multiplayer.player.left", chat.Text(p.Name)).SetColor(chat.Yellow), - pk.Byte(chatPosSystem), - pk.UUID(uuid.Nil), - ))) - delete(g.players, p.UUID) - } - } -} - -func (g *GlobalChat) broadcast(packet Packet758) { - for _, p := range g.players { - p.WritePacket(packet) - } -} - -func (g *GlobalChat) AddPlayer(player *Player) { g.join <- player } - -func (g *GlobalChat) RemovePlayer(p *Player) { g.quit <- p } - -func (c chatItem) toMessage() chat.Message { - return chat.TranslateMsg( - "chat.type.text", - chat.Message{ - Text: c.p.Name, - ClickEvent: chat.SuggestCommand("/msg " + c.p.Name), - HoverEvent: chat.ShowEntity(playerToSNBT(c.p)), - }, - chat.Text(c.text), - ) -} - -func playerToSNBT(p *Player) string { - var s nbt.StringifiedMessage - entity := struct { - ID string `nbt:"id"` - Name string `nbt:"name"` - }{ - ID: p.UUID.String(), - Name: p.Name, - } - - data, err := nbt.Marshal(entity) - if err != nil { - panic(err) - } - - err = nbt.Unmarshal(data, &s) - if err != nil { - panic(err) - } - - return string(s) -} diff --git a/server/player.go b/server/client.go similarity index 68% rename from server/player.go rename to server/client.go index 98d2616..cd57d70 100644 --- a/server/player.go +++ b/server/client.go @@ -2,54 +2,27 @@ package server import ( "container/list" - "github.com/google/uuid" "strconv" "sync" + "github.com/google/uuid" + "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/server/ecs" ) -type Player struct { +type Client struct { *net.Conn - - Name string - uuid.UUID - EntityID int32 - Gamemode byte - + Protocol int32 + ecs.Index packetQueue *PacketQueue errChan chan error } -func NewPlayer(conn *net.Conn, name string, id uuid.UUID, eid int32, gamemode byte) (p *Player) { - p = &Player{ - Conn: conn, - Name: name, - UUID: id, - EntityID: eid, - Gamemode: gamemode, - packetQueue: NewPacketQueue(), - errChan: make(chan error, 1), - } - go func() { - for { - packet, ok := p.packetQueue.Pull() - if !ok { - break - } - err := p.Conn.WritePacket(packet) - if err != nil { - p.PutErr(err) - break - } - } - }() - return -} - -func (p *Player) Close() { - p.packetQueue.Close() +type Player struct { + uuid.UUID + Name string } // Packet758 is a packet in protocol 757. @@ -58,8 +31,8 @@ type Packet758 pk.Packet type Packet757 pk.Packet // WritePacket to player client. The type of parameter will update per version. -func (p *Player) WritePacket(packet Packet758) { - p.packetQueue.Push(pk.Packet(packet)) +func (c *Client) WritePacket(packet Packet758) { + c.packetQueue.Push(pk.Packet(packet)) } type WritePacketError struct { @@ -75,17 +48,17 @@ func (s WritePacketError) Unwrap() error { return s.Err } -func (p *Player) PutErr(err error) { +func (c *Client) PutErr(err error) { select { - case p.errChan <- err: + case c.errChan <- err: default: // previous error exist, ignore this. } } -func (p *Player) GetErr() error { +func (c *Client) GetErr() error { select { - case err := <-p.errChan: + case err := <-c.errChan: return err default: return nil diff --git a/server/clientinfo/clientinfo.go b/server/clientinfo/clientinfo.go index 8c07b68..9ce299c 100644 --- a/server/clientinfo/clientinfo.go +++ b/server/clientinfo/clientinfo.go @@ -2,17 +2,14 @@ package clientinfo import ( "context" - - "github.com/google/uuid" + "github.com/Tnze/go-mc/server/ecs" "github.com/Tnze/go-mc/data/packetid" pk "github.com/Tnze/go-mc/net/packet" "github.com/Tnze/go-mc/server" ) -type ClientInformation struct { - Players map[uuid.UUID]*Info -} +type ClientInformation struct{} type Info struct { Locale string @@ -26,10 +23,25 @@ type Info struct { } func (c *ClientInformation) Init(g *server.Game) { - c.Players = make(map[uuid.UUID]*Info) + infos := ecs.GetComponent[Info](g.World) + type updateData struct { + eid ecs.Index + info Info + } + updateChan := make(chan updateData) + g.Add(ecs.FuncSystem(func() { + for { + select { + case info := <-updateChan: + infos.SetValue(info.eid, info.info) + default: + return + } + } + }), "ClientInfoSystem", nil) g.AddHandler(&server.PacketHandler{ ID: packetid.ServerboundClientInformation, - F: func(player *server.Player, p server.Packet758) error { + F: func(client *server.Client, player *server.Player, p server.Packet758) error { var ( Locale pk.String ViewDistance pk.Byte @@ -53,21 +65,25 @@ func (c *ClientInformation) Init(g *server.Game) { if err != nil { return err } - c.Players[player.UUID] = &Info{ - Locale: string(Locale), - ViewDistance: int(ViewDistance), - ChatMode: byte(ChatMode), - ChatColors: bool(ChatColors), - DisplayedSkinParts: byte(DisplayedSkinParts), - MainHand: byte(MainHand), - EnableTextFiltering: bool(EnableTextFiltering), - AllowServerListings: bool(AllowServerListings), + + updateChan <- updateData{ + eid: client.Index, + info: Info{ + Locale: string(Locale), + ViewDistance: int(ViewDistance), + ChatMode: byte(ChatMode), + ChatColors: bool(ChatColors), + DisplayedSkinParts: byte(DisplayedSkinParts), + MainHand: byte(MainHand), + EnableTextFiltering: bool(EnableTextFiltering), + AllowServerListings: bool(AllowServerListings), + }, } return nil }, }) } -func (c *ClientInformation) Run(ctx context.Context) {} -func (c *ClientInformation) AddPlayer(p *server.Player) {} -func (c *ClientInformation) RemovePlayer(p *server.Player) {} +func (c *ClientInformation) Run(ctx context.Context) {} +func (c *ClientInformation) ClientJoin(p *server.Client) {} +func (c *ClientInformation) ClientLeft(p *server.Client) {} diff --git a/server/command/component.go b/server/command/component.go index 8bf2297..66f1f2d 100644 --- a/server/command/component.go +++ b/server/command/component.go @@ -13,7 +13,7 @@ import ( func (g *Graph) Init(game *server.Game) { game.AddHandler(&server.PacketHandler{ ID: packetid.ServerboundChat, - F: func(player *server.Player, packet server.Packet758) error { + F: func(client *server.Client, player *server.Player, packet server.Packet758) error { var msg pk.String if err := pk.Packet(packet).Scan(&msg); err != nil { return err @@ -33,12 +33,12 @@ func (g *Graph) Init(game *server.Game) { // Run implement server.Component for Graph func (g *Graph) Run(ctx context.Context) {} -// AddPlayer implement server.Component for Graph -func (g *Graph) AddPlayer(p *server.Player) { - p.WritePacket(server.Packet758(pk.Marshal( +// ClientJoin implement server.Component for Graph +func (g *Graph) ClientJoin(client *server.Client, _ *server.Player) { + client.WritePacket(server.Packet758(pk.Marshal( packetid.ClientboundCommands, g, ))) } -// RemovePlayer implement server.Component for Graph -func (g *Graph) RemovePlayer(p *server.Player) {} +// ClientLeft implement server.Component for Graph +func (g *Graph) ClientLeft(_ *server.Client, _ *server.Player) {} diff --git a/server/dimension.go b/server/dimension.go index 5b24835..670bd6a 100644 --- a/server/dimension.go +++ b/server/dimension.go @@ -9,8 +9,8 @@ import ( type Level interface { Init(g *Game) Info() LevelInfo - PlayerJoin(p *Player) - PlayerQuit(p *Player) + PlayerJoin(p *Client) + PlayerQuit(p *Client) } type LevelInfo struct { @@ -43,7 +43,7 @@ func (s *SimpleDim) Info() LevelInfo { } } -func (s *SimpleDim) PlayerJoin(p *Player) { +func (s *SimpleDim) PlayerJoin(p *Client) { for pos, column := range s.columns { packet := pk.Marshal( packetid.ClientboundLevelChunkWithLight, @@ -53,4 +53,4 @@ func (s *SimpleDim) PlayerJoin(p *Player) { } } -func (s *SimpleDim) PlayerQuit(*Player) {} +func (s *SimpleDim) PlayerQuit(*Client) {} diff --git a/server/dimension/world.go b/server/dimension/world.go deleted file mode 100644 index b780da4..0000000 --- a/server/dimension/world.go +++ /dev/null @@ -1,32 +0,0 @@ -package dimension - -import ( - "github.com/Tnze/go-mc/server" -) - -type World struct { -} - -func (w *World) Init(g *server.Game) { - //TODO implement me - panic("implement me") -} - -func (w *World) Info() server.LevelInfo { - //TODO implement me - panic("implement me") -} - -func (w *World) PlayerJoin(p *server.Player) { - //TODO implement me - panic("implement me") -} - -func (w *World) PlayerQuit(p *server.Player) { - //TODO implement me - panic("implement me") -} - -func (w *World) register() { - -} diff --git a/server/ecs/bitest.go b/server/ecs/bitest.go new file mode 100644 index 0000000..c33b9f5 --- /dev/null +++ b/server/ecs/bitest.go @@ -0,0 +1,58 @@ +package ecs + +type BitSetLike interface { + Set(i Index) (old bool) + Unset(i Index) (old bool) + Contains(i Index) bool + And(other BitSetLike) (result BitSetLike) + AndNot(other BitSetLike) (result BitSetLike) + Range(f func(eid Index)) +} + +type BitSet struct { + // TODO: this is not a BitSet, I'm just testing + values map[Index]struct{} +} + +func (b BitSet) Set(i Index) (old bool) { + _, old = b.values[i] + b.values[i] = struct{}{} + return +} + +func (b BitSet) Unset(i Index) (old bool) { + _, old = b.values[i] + delete(b.values, i) + return +} + +func (b BitSet) Contains(i Index) bool { + _, contains := b.values[i] + return contains +} + +func (b BitSet) And(other BitSetLike) BitSetLike { + result := BitSet{values: make(map[Index]struct{})} + for i := range b.values { + if other.Contains(i) { + result.values[i] = struct{}{} + } + } + return result +} + +func (b BitSet) AndNot(other BitSetLike) BitSetLike { + result := BitSet{values: make(map[Index]struct{})} + for i := range b.values { + if !other.Contains(i) { + result.values[i] = struct{}{} + } + } + return result +} + +func (b BitSet) Range(f func(eid Index)) { + for i := range b.values { + f(i) + } +} diff --git a/server/ecs/dispatcher.go b/server/ecs/dispatcher.go new file mode 100644 index 0000000..3f0c2eb --- /dev/null +++ b/server/ecs/dispatcher.go @@ -0,0 +1,44 @@ +package ecs + +import "sync" + +type Dispatcher struct { + waiters map[string][]*sync.WaitGroup + tasks []func(w *World, wg *sync.WaitGroup) +} + +func NewDispatcher() *Dispatcher { + return &Dispatcher{ + waiters: make(map[string][]*sync.WaitGroup), + tasks: nil, + } +} + +func (d *Dispatcher) Add(s System, name string, deps []string) { + var start sync.WaitGroup + start.Add(len(deps)) + for _, dep := range deps { + if wg, ok := d.waiters[dep]; ok { + d.waiters[dep] = append(wg, &start) + } else { + panic("Unknown deps: " + dep) + } + } + d.tasks = append(d.tasks, func(w *World, done *sync.WaitGroup) { + start.Wait() + defer done.Done() + s.Update(w) + for _, wg := range d.waiters[name] { + wg.Done() + } + }) +} + +func (d *Dispatcher) Run(w *World) { + var wg sync.WaitGroup + wg.Add(len(d.tasks)) + for _, f := range d.tasks { + go f(w, &wg) + } + wg.Wait() +} diff --git a/server/ecs/ecs_test.go b/server/ecs/ecs_test.go new file mode 100644 index 0000000..b8374bb --- /dev/null +++ b/server/ecs/ecs_test.go @@ -0,0 +1,34 @@ +package ecs + +import "testing" + +func Test_common(t *testing.T) { + // W + w := NewWorld() + // C + type pos [2]int + type vel [2]int + Register[pos, *HashMapStorage[pos]](w) + Register[vel, *HashMapStorage[vel]](w) + // E + e1 := w.CreateEntity(pos{0, 0}) + w.CreateEntity(vel{1, 2}) + w.CreateEntity(pos{1, 2}, vel{2, 0}) + // S + s1 := FuncSystem(func(p *pos) { + t.Log("system 1", p) + }) + s2 := FuncSystem(func(p *pos, v *vel) { + t.Log("system 2", p, v) + }) + s3 := FuncSystem(func(p pos, v *vel) { + t.Log("system 2", p, v) + }) + // Run + s1.Update(w) + s2.Update(w) + s3.Update(w) + + w.DeleteEntity(e1) + s1.Update(w) +} diff --git a/server/ecs/storage.go b/server/ecs/storage.go new file mode 100644 index 0000000..12b99f7 --- /dev/null +++ b/server/ecs/storage.go @@ -0,0 +1,78 @@ +package ecs + +import ( + "reflect" + "strconv" + "unsafe" +) + +type Index uint32 + +type Storage[T any] interface { + Init() + GetValue(eid Index) *T + SetValue(eid Index, v T) + DelValue(eid Index) +} + +type HashMapStorage[T any] struct { + values map[Index]*T +} + +func (h *HashMapStorage[T]) Init() { h.values = make(map[Index]*T) } +func (h *HashMapStorage[T]) Len() int { return len(h.values) } +func (h *HashMapStorage[T]) GetValue(eid Index) *T { return h.values[eid] } +func (h *HashMapStorage[T]) SetValue(eid Index, v T) { h.values[eid] = &v } +func (h *HashMapStorage[T]) DelValue(eid Index) { delete(h.values, eid) } +func (h *HashMapStorage[T]) Range(f func(eid Index, value *T)) { + for i, v := range h.values { + f(i, v) + } +} + +type NullStorage[T any] struct{} + +func (NullStorage[T]) Init() { + var v T + if size := unsafe.Sizeof(v); size != 0 { + typeName := reflect.TypeOf(v).String() + typeSize := strconv.Itoa(int(size)) + panic("NullStorage can only be used with ZST, " + typeName + " has size of " + typeSize) + } +} +func (NullStorage[T]) GetValue(eid Index) *T { return nil } +func (NullStorage[T]) SetValue(eid Index, v T) {} +func (NullStorage[T]) DelValue(eid Index) {} + +type MaskedStorage[T any] struct { + BitSetLike + Storage[T] + Len int +} + +func (m *MaskedStorage[T]) Init() { + if m.BitSetLike == nil { + m.BitSetLike = BitSet{make(map[Index]struct{})} + } + m.Storage.Init() +} +func (m *MaskedStorage[T]) GetValue(eid Index) *T { + if m.Contains(eid) { + return m.Storage.GetValue(eid) + } + return nil +} +func (m *MaskedStorage[T]) GetValueAny(eid Index) any { return m.GetValue(eid) } +func (m *MaskedStorage[T]) SetValue(eid Index, v T) { + if !m.BitSetLike.Set(eid) { + m.Len++ + } + m.Storage.SetValue(eid, v) +} +func (m *MaskedStorage[T]) SetAny(eid Index, v any) { m.SetValue(eid, v.(T)) } +func (m *MaskedStorage[T]) DelValue(eid Index) { + if m.BitSetLike.Unset(eid) { + m.Len-- + } + m.Storage.DelValue(eid) +} diff --git a/server/ecs/system.go b/server/ecs/system.go new file mode 100644 index 0000000..d5a41d3 --- /dev/null +++ b/server/ecs/system.go @@ -0,0 +1,66 @@ +package ecs + +import ( + "reflect" +) + +type System interface { + Update(w *World) +} + +type funcsystem struct { + update func(w *World) +} + +func FuncSystem(F any) System { + type Storage interface { + BitSetLike + GetValueAny(eid Index) any + } + f := reflect.ValueOf(F) + in := f.Type().NumIn() + argTypes := make([]reflect.Type, in) + needCopy := make([]bool, in) + for i := 0; i < in; i++ { + if t := f.Type().In(i); t.Kind() == reflect.Pointer { + argTypes[i] = t.Elem() + } else { + argTypes[i] = t + needCopy[i] = true + } + } + return &funcsystem{ + update: func(w *World) { + storages := make([]Storage, in) + for i := 0; i < in; i++ { + storages[i] = w.GetResourceRaw(argTypes[i]).(Storage) + } + args := make([]reflect.Value, len(storages)) + if len(storages) > 0 { + set := BitSetLike(storages[0]) + for _, v := range storages[1:] { + set = set.And(v) + } + set.Range(func(eid Index) { + for i := range args { + arg := storages[i].GetValueAny(eid) + if arg == nil { + args[i] = reflect.Zero(argTypes[i]) + } else if needCopy[i] { + args[i] = reflect.ValueOf(arg).Elem() + } else { + args[i] = reflect.ValueOf(arg) + } + } + f.Call(args) + }) + } else { + f.Call(args) + } + }, + } +} + +func (f *funcsystem) Update(w *World) { + f.update(w) +} diff --git a/server/ecs/system_test.go b/server/ecs/system_test.go new file mode 100644 index 0000000..5b60e00 --- /dev/null +++ b/server/ecs/system_test.go @@ -0,0 +1,13 @@ +package ecs + +type PositionComponent struct { + X, Y int +} + +type MySystem1 struct { + *PositionComponent +} + +func (s *MySystem1) Update(w *World) { + +} diff --git a/server/ecs/world.go b/server/ecs/world.go new file mode 100644 index 0000000..d2d37d3 --- /dev/null +++ b/server/ecs/world.go @@ -0,0 +1,90 @@ +package ecs + +import ( + "reflect" + "sync/atomic" +) + +type World struct { + resources map[reflect.Type]any + maxEID Index +} + +func NewWorld() *World { + return &World{resources: make(map[reflect.Type]any)} +} + +func SetResource[Res any](w *World, v Res) *Res { + w.resources[reflect.TypeOf(v)] = &v + return &v +} + +func (w *World) Remove(resource any) any { + t := reflect.ValueOf(resource).Type() + resource = w.resources[t] + delete(w.resources, t) + return resource +} + +func GetResource[Res any](w *World) *Res { + var res Res + t := reflect.TypeOf(res) + if v, ok := w.resources[t]; ok { + return v.(*Res) + } + panic("Resource " + t.Name() + " not found") +} + +func (w *World) GetResourceRaw(t reflect.Type) any { + v, _ := w.resources[t] + return v +} + +func GetComponent[T any](w *World) *MaskedStorage[T] { + var value T + t := reflect.ValueOf(value).Type() + if res, ok := w.resources[t]; ok { + return res.(*MaskedStorage[T]) + } + panic("Component " + t.Name() + " not found") +} + +// Register the component with the storage. +// +// Will be changed to func (w *World) Register[C Component]() after Go support it +func Register[T any, S Storage[T]](w *World) { + var value T + t := reflect.TypeOf(value) + if _, ok := w.resources[t]; ok { + panic("Component " + t.Name() + " already exist") + } + var storage S + var storageInt Storage[T] + storageType := reflect.TypeOf(storage) + if storageType.Kind() == reflect.Pointer { + storageInt = reflect.New(storageType.Elem()).Interface().(Storage[T]) + } else { + storageInt = storage + } + ms := MaskedStorage[T]{Storage: storageInt} + ms.Init() + w.resources[t] = &ms +} + +func (w *World) CreateEntity(components ...any) (i Index) { + type Storage interface{ SetAny(Index, any) } + eid := Index(atomic.AddUint32((*uint32)(&w.maxEID), 1)) + for _, c := range components { + w.resources[reflect.TypeOf(c)].(Storage).SetAny(eid, c) + } + return eid +} + +func (w *World) DeleteEntity(eid Index) { + type Storage interface{ Del(eid Index) } + for _, r := range w.resources { + if c, ok := r.(Storage); ok { + c.Del(eid) + } + } +} diff --git a/server/entity.go b/server/entity.go new file mode 100644 index 0000000..0a5e758 --- /dev/null +++ b/server/entity.go @@ -0,0 +1,4 @@ +package server + +type Pos struct{ X, Y, Z float64 } +type Rot struct{ Yaw, Pitch float32 } diff --git a/server/gameplay.go b/server/gameplay.go index 06b452d..260ac64 100644 --- a/server/gameplay.go +++ b/server/gameplay.go @@ -3,15 +3,13 @@ package server import ( "context" _ "embed" - "sync" - "sync/atomic" + "time" "github.com/google/uuid" - "github.com/Tnze/go-mc/data/packetid" - "github.com/Tnze/go-mc/nbt" "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/server/ecs" ) type GamePlay interface { @@ -23,18 +21,10 @@ type GamePlay interface { } type Game struct { - Dim Level - components []Component + *ecs.World + *ecs.Dispatcher handlers map[int32][]*PacketHandler - - eid int32 -} - -type Component interface { - Init(g *Game) - Run(ctx context.Context) - AddPlayer(p *Player) - RemovePlayer(p *Player) + components []Component } type PacketHandler struct { @@ -42,24 +32,27 @@ type PacketHandler struct { F packetHandlerFunc } -type packetHandlerFunc func(player *Player, packet Packet758) error +type packetHandlerFunc func(client *Client, player *Player, packet Packet758) error -//go:embed DimensionCodec.snbt -var dimensionCodecSNBT nbt.StringifiedMessage +type Component interface { + Init(g *Game) + Run(ctx context.Context) + ClientJoin(c *Client, p *Player) + ClientLeft(c *Client, p *Player) +} -//go:embed Dimension.snbt -var dimensionSNBT nbt.StringifiedMessage - -func NewGame(dim Level, components ...Component) *Game { +func NewGame(components ...Component) *Game { g := &Game{ - Dim: dim, - components: components, + World: ecs.NewWorld(), + Dispatcher: ecs.NewDispatcher(), handlers: make(map[int32][]*PacketHandler), + components: components, } - dim.Init(g) - for _, v := range components { - v.Init(g) + for _, c := range components { + c.Init(g) } + ecs.Register[Client, *ecs.HashMapStorage[Client]](g.World) + ecs.Register[Player, *ecs.HashMapStorage[Player]](g.World) return g } @@ -68,73 +61,68 @@ func (g *Game) AddHandler(ph *PacketHandler) { } func (g *Game) Run(ctx context.Context) { - var wg sync.WaitGroup - wg.Add(len(g.components)) for _, c := range g.components { - go func(c Component) { - defer wg.Done() - c.Run(ctx) - }(c) + go c.Run(ctx) + } + ticker := time.NewTicker(time.Second / 20) + for { + select { + case <-ticker.C: + g.Dispatcher.Run(g.World) + case <-ctx.Done(): + return + } } - wg.Wait() } func (g *Game) AcceptPlayer(name string, id uuid.UUID, protocol int32, conn *net.Conn) { - p := NewPlayer(conn, name, id, g.newEID(), 1) - defer p.Close() - dimInfo := g.Dim.Info() - err := p.Conn.WritePacket(pk.Marshal( - packetid.ClientboundLogin, - pk.Int(p.EntityID), // Entity ID - pk.Boolean(false), // Is hardcore - pk.Byte(p.Gamemode), // Gamemode - pk.Byte(-1), // Prev Gamemode - pk.Array([]pk.Identifier{ - pk.Identifier(dimInfo.Name), - }), - pk.NBT(dimensionCodecSNBT), - pk.NBT(dimensionSNBT), - pk.Identifier(dimInfo.Name), // World Name - pk.Long(dimInfo.HashedSeed), // Hashed seed - pk.VarInt(0), // Max Players (Ignored by client) - pk.VarInt(15), // View Distance - pk.VarInt(15), // Simulation Distance - pk.Boolean(false), // Reduced Debug Info - pk.Boolean(true), // Enable respawn screen - pk.Boolean(false), // Is Debug - pk.Boolean(true), // Is Flat - )) - if err != nil { - return - } + eid := g.CreateEntity( + Client{ + Conn: conn, + Protocol: protocol, + packetQueue: NewPacketQueue(), + errChan: make(chan error, 1), + }, + Player{ + UUID: id, + Name: name, + }, + ) + c := ecs.GetComponent[Client](g.World).GetValue(eid) + p := ecs.GetComponent[Player](g.World).GetValue(eid) + defer c.packetQueue.Close() - g.Dim.PlayerJoin(p) - defer g.Dim.PlayerQuit(p) - - for _, c := range g.components { - c.AddPlayer(p) - if err := p.GetErr(); err != nil { - return + go func() { + for { + packet, ok := c.packetQueue.Pull() + if !ok { + break + } + err := c.Conn.WritePacket(packet) + if err != nil { + c.PutErr(err) + break + } } - defer c.RemovePlayer(p) + }() + + for _, component := range g.components { + component.ClientJoin(c, p) + defer component.ClientLeft(c, p) } var packet pk.Packet for { - if err := p.ReadPacket(&packet); err != nil { + if err := c.ReadPacket(&packet); err != nil { return } for _, ph := range g.handlers[packet.ID] { - if err := ph.F(p, Packet758(packet)); err != nil { + if err := ph.F(c, p, Packet758(packet)); err != nil { return } - if err := p.GetErr(); err != nil { + if err := c.GetErr(); err != nil { return } } } } - -func (g *Game) newEID() int32 { - return atomic.AddInt32(&g.eid, 1) -} diff --git a/server/internal/ecs/bitest.go b/server/internal/ecs/bitest.go deleted file mode 100644 index edda8d9..0000000 --- a/server/internal/ecs/bitest.go +++ /dev/null @@ -1,39 +0,0 @@ -package ecs - -type BitSet struct { - // TODO: this is not a bitset, I'm just testing - values map[Index]struct{} -} - -func (b BitSet) Set(i Index) (old bool) { - _, old = b.values[i] - b.values[i] = struct{}{} - return -} - -func (b BitSet) Unset(i Index) (old bool) { - _, old = b.values[i] - delete(b.values, i) - return -} - -func (b BitSet) Contains(i Index) bool { - _, contains := b.values[i] - return contains -} - -func (b BitSet) And(other BitSet) (result BitSet) { - result = BitSet{values: make(map[Index]struct{})} - for i := range b.values { - if other.Contains(i) { - result.values[i] = struct{}{} - } - } - return result -} - -func (b BitSet) Range(f func(eid Index)) { - for i := range b.values { - f(i) - } -} diff --git a/server/internal/ecs/ecs_test.go b/server/internal/ecs/ecs_test.go deleted file mode 100644 index 1744c11..0000000 --- a/server/internal/ecs/ecs_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package ecs - -import "testing" - -func Test_common(t *testing.T) { - // W - w := NewWorld() - // C - type pos [2]int - type vel [2]int - Register(w, pos{}) - Register(w, vel{}) - // E - w.CreateEntity(pos{0, 0}) - w.CreateEntity(vel{1, 2}) - w.CreateEntity(pos{1, 2}, vel{2, 0}) - // S - s1 := FuncSystem(func(p pos) { - t.Log("system 1", p) - }) - s2 := FuncSystem(func(p pos, v vel) { - t.Log("system 2", p, v) - }) - // Run - s1.Update(w) - s2.Update(w) -} diff --git a/server/internal/ecs/storage.go b/server/internal/ecs/storage.go deleted file mode 100644 index dbfc7a3..0000000 --- a/server/internal/ecs/storage.go +++ /dev/null @@ -1,34 +0,0 @@ -package ecs - -type Index uint32 - -type Storage interface { - Get(eid Index) any - Insert(eid Index, v any) - Remove(eid Index) any - BitSet() BitSet -} - -type HashMapStorage[T any] struct { - keys BitSet - values map[Index]T -} - -func NewHashMapStorage[T any]() *HashMapStorage[T] { - return &HashMapStorage[T]{ - keys: BitSet{values: make(map[Index]struct{})}, - values: make(map[Index]T), - } -} -func (h *HashMapStorage[T]) Get(eid Index) any { return h.values[eid] } -func (h *HashMapStorage[T]) Insert(eid Index, v any) { - h.keys.Set(eid) - h.values[eid] = v.(T) -} -func (h *HashMapStorage[T]) Remove(eid Index) any { - h.keys.Unset(eid) - v := h.values[eid] - delete(h.values, eid) - return v -} -func (h *HashMapStorage[T]) BitSet() BitSet { return h.keys } diff --git a/server/internal/ecs/system.go b/server/internal/ecs/system.go deleted file mode 100644 index e0f9b66..0000000 --- a/server/internal/ecs/system.go +++ /dev/null @@ -1,49 +0,0 @@ -package ecs - -import "reflect" - -type System interface { - Update(w *World) -} - -type funcsystem struct { - update reflect.Value - args func(w *World) []Storage -} - -func FuncSystem(F any) System { - f := reflect.ValueOf(F) - in := f.Type().NumIn() - argTypes := make([]reflect.Type, in) - for i := 0; i < in; i++ { - argTypes[i] = f.Type().In(i) - } - return &funcsystem{ - update: f, - args: func(w *World) (args []Storage) { - args = make([]Storage, in) - for i := 0; i < in; i++ { - args[i] = w.GetResourceRaw(argTypes[i]).(Storage) - } - return - }, - } -} - -func (f *funcsystem) Update(w *World) { - storages := f.args(w) - if len(storages) == 0 { - return - } - eids := storages[0].BitSet() - for _, v := range storages[1:] { - eids = eids.And(v.BitSet()) - } - args := make([]reflect.Value, len(storages)) - eids.Range(func(eid Index) { - for i := range args { - args[i] = reflect.ValueOf(storages[i].Get(eid)) - } - f.update.Call(args) - }) -} diff --git a/server/internal/ecs/world.go b/server/internal/ecs/world.go deleted file mode 100644 index 5acbf10..0000000 --- a/server/internal/ecs/world.go +++ /dev/null @@ -1,58 +0,0 @@ -package ecs - -import ( - "reflect" - "sync/atomic" -) - -type World struct { - resources map[reflect.Type]any - maxEID Index -} - -func NewWorld() *World { - return &World{resources: make(map[reflect.Type]any)} -} - -func (w *World) Insert(resource any) { - t := reflect.ValueOf(resource).Type() - w.resources[t] = resource -} - -func (w *World) Remove(resource any) any { - t := reflect.ValueOf(resource).Type() - resource = w.resources[t] - delete(w.resources, t) - return resource -} - -func (w *World) GetResource(resource any) any { - if resource == nil { - return nil - } - t := reflect.ValueOf(resource).Type() - v, _ := w.resources[t] - return v -} - -func (w *World) GetResourceRaw(t reflect.Type) any { - v, _ := w.resources[t] - return v -} - -func Register[T any](w *World, component T) { - t := reflect.TypeOf(component) - s := NewHashMapStorage[T]() - w.resources[t] = s -} - -func (w *World) CreateEntity(components ...any) (i Index) { - i = Index(atomic.AddUint32((*uint32)(&w.maxEID), 1)) - for _, c := range components { - v := reflect.ValueOf(c) - t := v.Type() - storage := w.resources[t].(Storage) - storage.Insert(w.maxEID, c) - } - return -} diff --git a/server/keepalive.go b/server/keepalive.go index b5378d3..a3edde3 100644 --- a/server/keepalive.go +++ b/server/keepalive.go @@ -4,11 +4,11 @@ import ( "container/list" "context" "errors" - "github.com/google/uuid" "time" "github.com/Tnze/go-mc/data/packetid" pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/server/ecs" ) // keepAliveInterval represents the interval when the server sends keep alive @@ -17,54 +17,60 @@ const keepAliveInterval = time.Second * 15 // keepAliveWaitInterval represents how long does the player expired const keepAliveWaitInterval = time.Second * 30 +type ClientDelay struct { + Delay time.Duration +} + type KeepAlive struct { - join chan *Player - quit chan *Player - tick chan *Player + join chan *Client + quit chan *Client + tick chan *Client pingList *list.List waitList *list.List - listIndex map[uuid.UUID]*list.Element + listIndex map[*Client]*list.Element listTimer *time.Timer waitTimer *time.Timer // The Notchian server uses a system-dependent time in milliseconds to generate the keep alive ID value. // We don't do that here for security reason. keepAliveID int64 - updatePlayerDelay func(p *Player, delay time.Duration) + updatePlayerDelay []func(p *Client, delay time.Duration) } func NewKeepAlive() (k *KeepAlive) { return &KeepAlive{ - join: make(chan *Player), - quit: make(chan *Player), - tick: make(chan *Player), + join: make(chan *Client), + quit: make(chan *Client), + tick: make(chan *Client), pingList: list.New(), waitList: list.New(), - listIndex: make(map[uuid.UUID]*list.Element), + listIndex: make(map[*Client]*list.Element), listTimer: time.NewTimer(keepAliveInterval), waitTimer: time.NewTimer(keepAliveWaitInterval), keepAliveID: 0, } } -func (k *KeepAlive) AddPlayerDelayUpdateHandler(f func(p *Player, delay time.Duration)) { - if k.updatePlayerDelay != nil { - panic("add player update handler twice") - } - k.updatePlayerDelay = f +func (k *KeepAlive) AddPlayerDelayUpdateHandler(f func(p *Client, delay time.Duration)) { + k.updatePlayerDelay = append(k.updatePlayerDelay, f) } // Init implement Component for KeepAlive func (k *KeepAlive) Init(g *Game) { + ecs.Register[ClientDelay, *ecs.HashMapStorage[ClientDelay]](g.World) + k.AddPlayerDelayUpdateHandler(func(p *Client, delay time.Duration) { + c := ClientDelay{Delay: delay} + ecs.GetComponent[ClientDelay](g.World).SetValue(p.Index, c) + }) g.AddHandler(&PacketHandler{ ID: packetid.ServerboundKeepAlive, - F: func(player *Player, packet Packet758) error { + F: func(client *Client, player *Player, packet Packet758) error { var KeepAliveID pk.Long if err := pk.Packet(packet).Scan(&KeepAliveID); err != nil { return err } - k.tick <- player + k.tick <- client return nil }, }) @@ -90,21 +96,21 @@ func (k *KeepAlive) Run(ctx context.Context) { } } -// AddPlayer implement Component for KeepAlive -func (k *KeepAlive) AddPlayer(player *Player) { k.join <- player } +// ClientJoin implement Component for KeepAlive +func (k *KeepAlive) ClientJoin(client *Client, _ *Player) { k.join <- client } -// RemovePlayer implement Component for KeepAlive -func (k *KeepAlive) RemovePlayer(p *Player) { k.quit <- p } +// ClientLeft implement Component for KeepAlive +func (k *KeepAlive) ClientLeft(client *Client, _ *Player) { k.quit <- client } -func (k KeepAlive) pushPlayer(p *Player) { - k.listIndex[p.UUID] = k.pingList.PushBack( +func (k KeepAlive) pushPlayer(p *Client) { + k.listIndex[p] = k.pingList.PushBack( keepAliveItem{player: p, t: time.Now()}, ) } -func (k *KeepAlive) removePlayer(p *Player) { - elem := k.listIndex[p.UUID] - delete(k.listIndex, p.UUID) +func (k *KeepAlive) removePlayer(p *Client) { + elem := k.listIndex[p] + delete(k.listIndex, p) if elem.Prev() == nil { // At present, it is difficult to distinguish // which linked list the player is in, @@ -126,7 +132,7 @@ func (k *KeepAlive) pingPlayer(now time.Time) { ))) k.keepAliveID++ // Clientbound KeepAlive packet is sent, move the player to waiting list. - k.listIndex[p.UUID] = k.waitList.PushBack( + k.listIndex[p] = k.waitList.PushBack( keepAliveItem{player: p, t: now}, ) } @@ -134,10 +140,10 @@ func (k *KeepAlive) pingPlayer(now time.Time) { keepAliveSetTimer(k.pingList, k.listTimer, keepAliveInterval) } -func (k *KeepAlive) tickPlayer(p *Player) { - elem, ok := k.listIndex[p.UUID] +func (k *KeepAlive) tickPlayer(p *Client) { + elem, ok := k.listIndex[p] if !ok { - p.PutErr(errors.New("keepalive: fail to tick player: " + p.UUID.String() + " not found")) + p.PutErr(errors.New("keepalive: fail to tick player: client not found")) return } if elem.Prev() == nil { @@ -147,13 +153,13 @@ func (k *KeepAlive) tickPlayer(p *Player) { defer keepAliveSetTimer(k.waitList, k.waitTimer, keepAliveWaitInterval) } // update delay of player - t := k.waitList.Remove(elem).(keepAliveItem).t now := time.Now() - if k.updatePlayerDelay != nil { - k.updatePlayerDelay(p, now.Sub(t)) + delay := now.Sub(k.waitList.Remove(elem).(keepAliveItem).t) + for _, f := range k.updatePlayerDelay { + f(p, delay) } // move the player to ping list - k.listIndex[p.UUID] = k.pingList.PushBack( + k.listIndex[p] = k.pingList.PushBack( keepAliveItem{player: p, t: now}, ) } @@ -180,6 +186,6 @@ func keepAliveSetTimer(l *list.List, timer *time.Timer, interval time.Duration) } type keepAliveItem struct { - player *Player + player *Client t time.Time } diff --git a/server/ping.go b/server/ping.go index 6ca8e6f..06c232f 100644 --- a/server/ping.go +++ b/server/ping.go @@ -4,14 +4,16 @@ import ( "encoding/base64" "encoding/json" "errors" + "image" + "image/png" + "strings" + + "github.com/google/uuid" + "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/net" pk "github.com/Tnze/go-mc/net/packet" - "github.com/google/uuid" - "image" - "image/png" - "strings" ) // ListPingHandler collect server running status info diff --git a/server/player/player.go b/server/player/player.go new file mode 100644 index 0000000..bd1fb94 --- /dev/null +++ b/server/player/player.go @@ -0,0 +1,112 @@ +package player + +import ( + "fmt" + "github.com/Tnze/go-mc/data/packetid" + pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/server" + "github.com/Tnze/go-mc/server/ecs" + "github.com/Tnze/go-mc/server/world" + "log" +) + +type PlayerProfile struct { + Dim ecs.Index +} + +type playerSpawnSystem struct { + storage +} + +func (p playerSpawnSystem) Update(w *ecs.World) { + clients := ecs.GetComponent[server.Client](w) + players := ecs.GetComponent[server.Player](w) + profiles := ecs.GetComponent[PlayerProfile](w) + dimensionRes := ecs.GetResource[world.DimensionList](w) + players.AndNot(profiles.BitSetLike).Range(func(eid ecs.Index) { + player := players.GetValue(eid) + client := clients.GetValue(eid) + profile, err := p.GetPlayer(player.UUID) + if err != nil { + client.PutErr(fmt.Errorf("read player data fail: %w", err)) + return + } + log.Println("load player info successes", profile) + dim, ok := dimensionRes.Find(profile.Dimension) + if !ok { + panic("dimension " + profile.Dimension + " not found") + } + profiles.SetValue(eid, PlayerProfile{Dim: dim}) + client.WritePacket(server.Packet758(pk.Marshal( + packetid.ClientboundLogin, + pk.Int(eid), // Entity ID + pk.Boolean(false), // Is hardcore + pk.Byte(profile.PlayerGameType), // Gamemode + pk.Byte(-1), // Prev Gamemode + dimensionRes, + pk.NBT(dimensionRes.DimCodecSNBT), + pk.NBT(dimensionRes.DimSNBT), + pk.Identifier(profile.Dimension), // World Name + pk.Long(1234567), // Hashed seed + pk.VarInt(0), // Max Players (Ignored by client) + pk.VarInt(15), // View Distance + pk.VarInt(15), // Simulation Distance + pk.Boolean(false), // Reduced Debug Info + pk.Boolean(true), // Enable respawn screen + pk.Boolean(false), // Is Debug + pk.Boolean(true), // Is Flat + ))) + }) +} + +func SpawnSystem(g *server.Game, playerdataPath string) { + ecs.Register[PlayerProfile, *ecs.HashMapStorage[PlayerProfile]](g.World) + g.Dispatcher.Add(playerSpawnSystem{storage: storage{playerdataPath}}, "go-mc:player:SpawnSystem", nil) +} + +// PosAndRotSystem add a system to g.Dispatcher that +// receive player movement packets and update Pos and Rot component +// Require component Pos and Rot to be registered before. +func PosAndRotSystem(g *server.Game) { + type posUpdate struct { + ecs.Index + server.Pos + } + updateChan := make(chan posUpdate) + ecs.Register[server.Pos, *ecs.HashMapStorage[server.Pos]](g.World) + ecs.Register[server.Rot, *ecs.HashMapStorage[server.Rot]](g.World) + g.Dispatcher.Add(ecs.FuncSystem(func() { + posStorage := ecs.GetComponent[server.Pos](g.World) + for { + select { + case event := <-updateChan: + if v := posStorage.GetValue(event.Index); v != nil { + *v = event.Pos + } + default: + return + } + } + }), "go-mc:player:PosAndRotSystem", nil) + + g.AddHandler(&server.PacketHandler{ + ID: packetid.ServerboundMovePlayerPos, + F: func(client *server.Client, player *server.Player, packet server.Packet758) error { + var X, FeetY, Z pk.Double + var OnGround pk.Boolean + err := pk.Packet(packet).Scan(&X, &FeetY, &Z, &OnGround) + if err != nil { + return err + } + updateChan <- posUpdate{ + Index: client.Index, + Pos: server.Pos{ + X: float64(X), + Y: float64(FeetY), + Z: float64(Z), + }, + } + return nil + }, + }) +} diff --git a/server/player/playerinfo.go b/server/player/playerinfo.go new file mode 100644 index 0000000..dbb145a --- /dev/null +++ b/server/player/playerinfo.go @@ -0,0 +1,163 @@ +package player + +//import ( +// "context" +// "io" +// "time" +// +// "github.com/Tnze/go-mc/data/packetid" +// pk "github.com/Tnze/go-mc/net/packet" +// "github.com/Tnze/go-mc/server" +// "github.com/Tnze/go-mc/server/ecs" +//) +// +//type PlayerInfo struct { +// updateDelay chan playerDelayUpdate +// quit chan clientAndPlayer +//} +//type clientAndPlayer struct { +// *server.Client +// *server.Player +//} +// +//type playerInfoList struct { +// players ecs.MaskedStorage[server.Player] +// delays ecs.MaskedStorage[server.ClientDelay] +//} +// +//func (p playerInfoList) WriteTo(w io.Writer) (n int64, err error) { +// n, err = pk.VarInt(p.players.Len).WriteTo(w) +// if err != nil { +// return +// } +// var n1 int64 +// p.players.And(p.delays.BitSetLike).Range(func(eid ecs.Index) { +// p := playerDelayUpdate{ +// player: p.players.Get(eid), +// delay: p.delays.Get(eid).Delay, +// } +// n1, err = p.WriteTo(w) +// n += n1 +// if err != nil { +// return +// } +// }) +// return +//} +// +//type playerDelayUpdate struct { +// player *server.Player +// delay time.Duration +//} +// +//func (p playerDelayUpdate) WriteTo(w io.Writer) (n int64, err error) { +// return pk.Tuple{ +// pk.UUID(p.player.UUID), +// pk.VarInt(p.delay.Milliseconds()), +// }.WriteTo(w) +//} +// +//const ( +// actionAddPlayer = iota +// actionUpdateGamemode +// actionUpdateLatency +// actionUpdateDisplayName +// actionRemovePlayer +//) +// +//type DelaySource interface { +// AddPlayerDelayUpdateHandler(f func(c *server.Client, p *server.Player, delay time.Duration)) +//} +// +//func NewPlayerInfo(delaySource DelaySource) *PlayerInfo { +// updateChan := make(chan playerDelayUpdate) +// p := &PlayerInfo{ +// updateDelay: updateChan, +// quit: make(chan clientAndPlayer), +// } +// if delaySource != nil { +// delaySource.AddPlayerDelayUpdateHandler(func(client *server.Client, player *server.Player, delay time.Duration) { +// updateChan <- playerDelayUpdate{player: player, delay: delay} +// }) +// } +// return p +//} +// +//type playerInfoSystemJoin struct{} +// +//func (p playerInfoSystemJoin) Update(w *ecs.World) { +// clients := ecs.GetComponent[server.Client](w) +// players := ecs.GetComponent[server.Player](w) +// delays := ecs.GetComponent[server.ClientDelay](w) +//} +// +//func (p *PlayerInfo) Init(g *server.Game) { +// var delayBuffer []playerDelayUpdate +// clients := ecs.GetComponent[server.Client](g.World) +// players := ecs.GetComponent[server.Player](g.World) +// delays := ecs.GetComponent[server.ClientDelay](g.World) +// g.Dispatcher.Add(ecs.FuncSystem(func(client *server.Client, player *server.Player, delay server.ClientDelay) { +// info := server.ClientDelay{Delay: 0} +// pack := server.Packet758(pk.Marshal( +// packetid.ClientboundPlayerInfo, +// pk.VarInt(actionAddPlayer), +// pk.VarInt(1), +// +// pk.UUID(player.UUID), +// pk.String(player.Name), +// pk.Array([]pk.FieldEncoder{}), +// pk.VarInt(profile.Gamemode), +// pk.VarInt(0), +// pk.Boolean(false), +// )) +// delays.Set(client.Index, info) +// clients.Range(func(eid ecs.Index) { +// clients.Get(eid).WritePacket(pack) +// }) +// client.WritePacket(server.Packet758(pk.Marshal( +// packetid.ClientboundPlayerInfo, +// pk.VarInt(actionAddPlayer), +// playerInfoList{players: players, delays: delays}, +// ))) +// }), "PlayerInfoSystem:Join", nil) +// g.Dispatcher.Add(ecs.FuncSystem(func() { +// for { +// select { +// case cp := <-p.quit: +// pack := server.Packet758(pk.Marshal( +// packetid.ClientboundPlayerInfo, +// pk.VarInt(actionRemovePlayer), +// pk.VarInt(1), +// pk.UUID(cp.UUID), +// )) +// for _, p := range players.list { +// cp.WritePacket(pack) +// } +// case change := <-p.updateDelay: +// delayBuffer = append(delayBuffer, change) +// default: +// if len(delayBuffer) > 0 { +// pack := server.Packet758(pk.Marshal( +// packetid.ClientboundPlayerInfo, +// pk.VarInt(actionUpdateLatency), +// pk.Array(&delayBuffer), +// )) +// players.Range(func(eid ecs.Index) { +// players.Get(eid).(*server.Client).WritePacket(pack) +// }) +// delayBuffer = delayBuffer[:0] +// } +// return +// } +// } +// }), "PlayerInfoSystem", nil) +//} +// +//func (p *PlayerInfo) Run(context.Context) {} +//func (p *PlayerInfo) ClientJoin(client *server.Client, player *server.Player) {} +//func (p *PlayerInfo) ClientLeft(client *server.Client, player *server.Player) { +// p.quit <- clientAndPlayer{ +// Client: client, +// Player: player, +// } +//} diff --git a/server/player/pool.go b/server/player/pool.go new file mode 100644 index 0000000..76f40ed --- /dev/null +++ b/server/player/pool.go @@ -0,0 +1,37 @@ +package player + +import ( + "compress/gzip" + "os" + "path/filepath" + + "github.com/google/uuid" + + "github.com/Tnze/go-mc/level" + "github.com/Tnze/go-mc/save" +) + +type storage struct { + playerdataDir string +} + +func (s *storage) GetPlayer(id uuid.UUID) (data save.PlayerData, err error) { + filename := id.String() + ".dat" + + f, err := os.Open(filepath.Join(s.playerdataDir, filename)) + if err != nil { + return save.PlayerData{}, err + } + defer f.Close() + + r, err := gzip.NewReader(f) + if err != nil { + return save.PlayerData{}, err + } + + return save.ReadPlayerData(r) +} + +func (s *storage) PutPlayer(pos level.ChunkPos, c *level.Chunk) (err error) { + return nil +} diff --git a/server/playerinfo.go b/server/playerinfo.go deleted file mode 100644 index 559a438..0000000 --- a/server/playerinfo.go +++ /dev/null @@ -1,148 +0,0 @@ -package server - -import ( - "context" - "io" - "time" - - "github.com/google/uuid" - - "github.com/Tnze/go-mc/data/packetid" - pk "github.com/Tnze/go-mc/net/packet" -) - -type PlayerInfo struct { - updateDelay chan playerDelayInfo - join chan *Player - quit chan *Player - ticker *time.Ticker -} - -type playerDelayInfo struct { - player *Player - delay time.Duration -} - -func (p *playerDelayInfo) WriteTo(w io.Writer) (n int64, err error) { - return pk.Tuple{ - pk.UUID(p.player.UUID), - pk.String(p.player.Name), - pk.Array([]pk.FieldEncoder{}), - pk.VarInt(p.player.Gamemode), - pk.VarInt(p.delay), - pk.Boolean(false), - }.WriteTo(w) -} - -type playerInfoList struct { - list map[uuid.UUID]playerDelayInfo -} - -func (p *playerInfoList) WriteTo(w io.Writer) (n int64, err error) { - n, err = pk.VarInt(len(p.list)).WriteTo(w) - if err != nil { - return - } - var n1 int64 - for _, p := range p.list { - n1, err = p.WriteTo(w) - n += n1 - if err != nil { - return - } - } - return -} - -type playerDelayUpdate playerDelayInfo - -func (p playerDelayUpdate) WriteTo(w io.Writer) (n int64, err error) { - return pk.Tuple{ - pk.UUID(p.player.UUID), - pk.VarInt(p.delay.Milliseconds()), - }.WriteTo(w) -} - -const ( - actionAddPlayer = iota - actionUpdateGamemode - actionUpdateLatency - actionUpdateDisplayName - actionRemovePlayer -) - -type PlayerDelaySource interface { - AddPlayerDelayUpdateHandler(f func(p *Player, delay time.Duration)) -} - -func NewPlayerInfo(updateFreq time.Duration, delaySource PlayerDelaySource) *PlayerInfo { - p := &PlayerInfo{ - updateDelay: make(chan playerDelayInfo), - join: make(chan *Player), - quit: make(chan *Player), - ticker: time.NewTicker(updateFreq), - } - if delaySource != nil { - delaySource.AddPlayerDelayUpdateHandler(p.onPlayerDelayUpdate) - } - return p -} - -func (p *PlayerInfo) Init(*Game) {} - -func (p *PlayerInfo) Run(ctx context.Context) { - players := &playerInfoList{list: make(map[uuid.UUID]playerDelayInfo)} - var delayBuffer []playerDelayUpdate - for { - select { - case player := <-p.join: - info := playerDelayInfo{player: player, delay: 0} - pack := Packet758(pk.Marshal( - packetid.ClientboundPlayerInfo, - pk.VarInt(actionAddPlayer), - pk.VarInt(1), - &info, - )) - players.list[player.UUID] = info - for _, p := range players.list { - p.player.WritePacket(pack) - } - player.WritePacket(Packet758(pk.Marshal( - packetid.ClientboundPlayerInfo, - pk.VarInt(actionAddPlayer), - players, - ))) - case player := <-p.quit: - delete(players.list, player.UUID) - pack := Packet758(pk.Marshal( - packetid.ClientboundPlayerInfo, - pk.VarInt(actionRemovePlayer), - pk.VarInt(1), - pk.UUID(player.UUID), - )) - for _, p := range players.list { - p.player.WritePacket(pack) - } - case change := <-p.updateDelay: - delayBuffer = append(delayBuffer, playerDelayUpdate(change)) - case <-p.ticker.C: - pack := Packet758(pk.Marshal( - packetid.ClientboundPlayerInfo, - pk.VarInt(actionUpdateLatency), - pk.Array(&delayBuffer), - )) - for _, p := range players.list { - p.player.WritePacket(pack) - } - delayBuffer = delayBuffer[:0] - case <-ctx.Done(): - break - } - } -} - -func (p *PlayerInfo) AddPlayer(player *Player) { p.join <- player } -func (p *PlayerInfo) RemovePlayer(player *Player) { p.quit <- player } -func (p *PlayerInfo) onPlayerDelayUpdate(player *Player, delay time.Duration) { - p.updateDelay <- playerDelayInfo{player: player, delay: delay} -} diff --git a/server/playerlist.go b/server/playerlist.go index 4a29055..5181ac1 100644 --- a/server/playerlist.go +++ b/server/playerlist.go @@ -16,7 +16,7 @@ import ( // This struct should not be copied after used. type PlayerList struct { maxPlayer int - players map[uuid.UUID]*Player + clients map[uuid.UUID]*Player // Only the field players is protected by this Mutex. // Because others field never change after created. playersLock sync.Mutex @@ -26,7 +26,7 @@ type PlayerList struct { func NewPlayerList(maxPlayers int) *PlayerList { return &PlayerList{ maxPlayer: maxPlayers, - players: make(map[uuid.UUID]*Player), + clients: make(map[uuid.UUID]*Player), } } @@ -36,35 +36,35 @@ func (p *PlayerList) Init(*Game) {} // Run implement Component for PlayerList func (p *PlayerList) Run(context.Context) {} -// AddPlayer implement Component for PlayerList -func (p *PlayerList) AddPlayer(player *Player) { +// ClientJoin implement Component for PlayerList +func (p *PlayerList) ClientJoin(client *Client, player *Player) { p.playersLock.Lock() defer p.playersLock.Unlock() - if len(p.players) >= p.maxPlayer { - player.WritePacket(Packet758(pk.Marshal( + if len(p.clients) >= p.maxPlayer { + client.WritePacket(Packet758(pk.Marshal( packetid.ClientboundDisconnect, chat.TranslateMsg("multiplayer.disconnect.server_full"), ))) - player.PutErr(errors.New("playerlist: server full")) + client.PutErr(errors.New("playerlist: server full")) return } - p.players[player.UUID] = player + p.clients[player.UUID] = player } -// RemovePlayer implement Component for PlayerList -func (p *PlayerList) RemovePlayer(player *Player) { +// ClientLeft implement Component for PlayerList +func (p *PlayerList) ClientLeft(_ *Client, player *Player) { p.playersLock.Lock() defer p.playersLock.Unlock() - delete(p.players, player.UUID) + delete(p.clients, player.UUID) } // CheckPlayer implement LoginChecker for PlayerList func (p *PlayerList) CheckPlayer(name string, id uuid.UUID, protocol int32) (ok bool, reason chat.Message) { p.playersLock.Lock() defer p.playersLock.Unlock() - if len(p.players) >= p.maxPlayer { + if len(p.clients) >= p.maxPlayer { return false, chat.TranslateMsg("multiplayer.disconnect.server_full") } return true, chat.Message{} @@ -77,20 +77,20 @@ func (p *PlayerList) MaxPlayer() int { func (p *PlayerList) OnlinePlayer() int { p.playersLock.Lock() defer p.playersLock.Unlock() - return len(p.players) + return len(p.clients) } func (p *PlayerList) PlayerSamples() (sample []PlayerSample) { p.playersLock.Lock() defer p.playersLock.Unlock() // Up to 10 players can be returned - length := len(p.players) + length := len(p.clients) if length > 10 { length = 10 } sample = make([]PlayerSample, length) var i int - for _, v := range p.players { + for _, v := range p.clients { sample[i] = PlayerSample{ Name: v.Name, ID: v.UUID, diff --git a/server/Dimension.snbt b/server/world/Dimension.snbt similarity index 100% rename from server/Dimension.snbt rename to server/world/Dimension.snbt diff --git a/server/DimensionCodec.snbt b/server/world/DimensionCodec.snbt similarity index 100% rename from server/DimensionCodec.snbt rename to server/world/DimensionCodec.snbt diff --git a/server/dimension/pool.go b/server/world/pool.go similarity index 98% rename from server/dimension/pool.go rename to server/world/pool.go index b435fc8..7ac3225 100644 --- a/server/dimension/pool.go +++ b/server/world/pool.go @@ -1,4 +1,4 @@ -package dimension +package world import ( "fmt" diff --git a/server/world/world.go b/server/world/world.go new file mode 100644 index 0000000..059f2e9 --- /dev/null +++ b/server/world/world.go @@ -0,0 +1,51 @@ +package world + +import ( + _ "embed" + "github.com/Tnze/go-mc/server" + "io" + "unsafe" + + "github.com/Tnze/go-mc/nbt" + pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/server/ecs" +) + +//go:embed DimensionCodec.snbt +var dimensionCodecSNBT nbt.StringifiedMessage + +//go:embed Dimension.snbt +var dimensionSNBT nbt.StringifiedMessage + +type Dimension struct { + storage + Name string +} + +type DimensionList struct { + Dims []ecs.Index + DimNames []string + DimCodecSNBT, DimSNBT nbt.StringifiedMessage +} + +func (d *DimensionList) WriteTo(w io.Writer) (n int64, err error) { + return pk.Array(*(*[]pk.Identifier)(unsafe.Pointer(&d.DimNames))).WriteTo(w) +} + +func (d *DimensionList) Find(dim string) (ecs.Index, bool) { + for i, v := range d.DimNames { + if v == dim { + return d.Dims[i], true + } + } + return 0, false +} + +func NewDimensionManager(g *server.Game) *DimensionList { + return ecs.SetResource(g.World, DimensionList{ + Dims: nil, + DimNames: nil, + DimCodecSNBT: dimensionCodecSNBT, + DimSNBT: dimensionSNBT, + }) +}