player and dimension loader with ecs system

This commit is contained in:
Tnze
2022-05-27 00:38:46 +08:00
parent d2f7db9d0d
commit 474d6a229b
34 changed files with 956 additions and 795 deletions

View File

@ -4,20 +4,16 @@ import (
"context" "context"
_ "embed" _ "embed"
"flag" "flag"
"fmt" "github.com/Tnze/go-mc/server/world"
"image" "image"
_ "image/png" _ "image/png"
"log" "log"
"os" "os"
"path/filepath"
"time"
"github.com/Tnze/go-mc/chat" "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"
"github.com/Tnze/go-mc/server/command" "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"}}} 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() keepAlive := server.NewKeepAlive()
playerInfo := server.NewPlayerInfo(time.Second, keepAlive) //playerInfo := player.NewPlayerInfo(keepAlive)
defaultDimension, err := loadAllRegions(*regionPath)
if err != nil {
log.Fatalf("Load chunks fail: %v", err)
}
commands := command.NewGraph() commands := command.NewGraph()
handleFunc := func(ctx context.Context, args []command.ParsedData) error { handleFunc := func(ctx context.Context, args []command.ParsedData) error {
@ -59,21 +51,21 @@ func main() {
HandleFunc(handleFunc)). HandleFunc(handleFunc)).
HandleFunc(handleFunc), HandleFunc(handleFunc),
) )
game := server.NewGame( game := server.NewGame(
defaultDimension,
playerList, playerList,
playerInfo, //playerInfo,
keepAlive, keepAlive,
server.NewGlobalChat(),
commands, commands,
) )
world.NewDimensionManager(game)
player.SpawnSystem(game, "./save/testdata/playerdata")
player.PosAndRotSystem(game)
go game.Run(context.Background()) go game.Run(context.Background())
s := server.Server{ s := server.Server{
ListPingHandler: serverInfo, ListPingHandler: serverInfo,
LoginHandler: &server.MojangLoginHandler{ LoginHandler: &server.MojangLoginHandler{
OnlineMode: false, OnlineMode: true,
Threshold: 256, Threshold: 256,
LoginChecker: playerList, LoginChecker: playerList,
}, },
@ -100,52 +92,3 @@ func readIcon() image.Image {
} }
return icon 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
}

View File

@ -253,13 +253,18 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error {
vt := val.Type() //receiver must be []int or []int32 vt := val.Type() //receiver must be []int or []int32
if vt.Kind() == reflect.Interface { if vt.Kind() == reflect.Interface {
vt = reflect.TypeOf([]int32{}) // pass 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") 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 { } else if tk := val.Type().Elem().Kind(); tk != reflect.Int && tk != reflect.Int32 {
return errors.New("cannot parse TagIntArray to " + vt.String()) 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++ { for i := 0; i < int(aryLen); i++ {
value, err := d.readInt() value, err := d.readInt()
if err != nil { if err != nil {
@ -267,7 +272,9 @@ func (d *Decoder) unmarshal(val reflect.Value, tagType byte) error {
} }
buf.Index(i).SetInt(int64(value)) buf.Index(i).SetInt(int64(value))
} }
val.Set(buf) if vt.Kind() == reflect.Slice {
val.Set(buf)
}
case TagLongArray: case TagLongArray:
aryLen, err := d.readInt() aryLen, err := d.readInt()

View File

@ -1,9 +1,7 @@
package save package save
import ( import (
"encoding/binary"
"github.com/Tnze/go-mc/nbt" "github.com/Tnze/go-mc/nbt"
"github.com/google/uuid"
"io" "io"
) )
@ -18,8 +16,7 @@ type PlayerData struct {
FallFlying byte FallFlying byte
OnGround byte OnGround byte
UUID uuid.UUID `nbt:"-"` UUID [4]int32
UUIDLeast, UUIDMost int64
PlayerGameType int32 `nbt:"playerGameType"` PlayerGameType int32 `nbt:"playerGameType"`
Air int16 Air int16
@ -80,8 +77,5 @@ type Item struct {
func ReadPlayerData(r io.Reader) (data PlayerData, err error) { func ReadPlayerData(r io.Reader) (data PlayerData, err error) {
_, err = nbt.NewDecoder(r).Decode(&data) _, 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 return
} }

View File

@ -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)
}

View File

@ -2,54 +2,27 @@ package server
import ( import (
"container/list" "container/list"
"github.com/google/uuid"
"strconv" "strconv"
"sync" "sync"
"github.com/google/uuid"
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/server/ecs"
) )
type Player struct { type Client struct {
*net.Conn *net.Conn
Protocol int32
Name string ecs.Index
uuid.UUID
EntityID int32
Gamemode byte
packetQueue *PacketQueue packetQueue *PacketQueue
errChan chan error errChan chan error
} }
func NewPlayer(conn *net.Conn, name string, id uuid.UUID, eid int32, gamemode byte) (p *Player) { type Player struct {
p = &Player{ uuid.UUID
Conn: conn, Name string
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()
} }
// Packet758 is a packet in protocol 757. // Packet758 is a packet in protocol 757.
@ -58,8 +31,8 @@ type Packet758 pk.Packet
type Packet757 pk.Packet type Packet757 pk.Packet
// WritePacket to player client. The type of parameter will update per version. // WritePacket to player client. The type of parameter will update per version.
func (p *Player) WritePacket(packet Packet758) { func (c *Client) WritePacket(packet Packet758) {
p.packetQueue.Push(pk.Packet(packet)) c.packetQueue.Push(pk.Packet(packet))
} }
type WritePacketError struct { type WritePacketError struct {
@ -75,17 +48,17 @@ func (s WritePacketError) Unwrap() error {
return s.Err return s.Err
} }
func (p *Player) PutErr(err error) { func (c *Client) PutErr(err error) {
select { select {
case p.errChan <- err: case c.errChan <- err:
default: default:
// previous error exist, ignore this. // previous error exist, ignore this.
} }
} }
func (p *Player) GetErr() error { func (c *Client) GetErr() error {
select { select {
case err := <-p.errChan: case err := <-c.errChan:
return err return err
default: default:
return nil return nil

View File

@ -2,17 +2,14 @@ package clientinfo
import ( import (
"context" "context"
"github.com/Tnze/go-mc/server/ecs"
"github.com/google/uuid"
"github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/server" "github.com/Tnze/go-mc/server"
) )
type ClientInformation struct { type ClientInformation struct{}
Players map[uuid.UUID]*Info
}
type Info struct { type Info struct {
Locale string Locale string
@ -26,10 +23,25 @@ type Info struct {
} }
func (c *ClientInformation) Init(g *server.Game) { 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{ g.AddHandler(&server.PacketHandler{
ID: packetid.ServerboundClientInformation, 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 ( var (
Locale pk.String Locale pk.String
ViewDistance pk.Byte ViewDistance pk.Byte
@ -53,21 +65,25 @@ func (c *ClientInformation) Init(g *server.Game) {
if err != nil { if err != nil {
return err return err
} }
c.Players[player.UUID] = &Info{
Locale: string(Locale), updateChan <- updateData{
ViewDistance: int(ViewDistance), eid: client.Index,
ChatMode: byte(ChatMode), info: Info{
ChatColors: bool(ChatColors), Locale: string(Locale),
DisplayedSkinParts: byte(DisplayedSkinParts), ViewDistance: int(ViewDistance),
MainHand: byte(MainHand), ChatMode: byte(ChatMode),
EnableTextFiltering: bool(EnableTextFiltering), ChatColors: bool(ChatColors),
AllowServerListings: bool(AllowServerListings), DisplayedSkinParts: byte(DisplayedSkinParts),
MainHand: byte(MainHand),
EnableTextFiltering: bool(EnableTextFiltering),
AllowServerListings: bool(AllowServerListings),
},
} }
return nil return nil
}, },
}) })
} }
func (c *ClientInformation) Run(ctx context.Context) {} func (c *ClientInformation) Run(ctx context.Context) {}
func (c *ClientInformation) AddPlayer(p *server.Player) {} func (c *ClientInformation) ClientJoin(p *server.Client) {}
func (c *ClientInformation) RemovePlayer(p *server.Player) {} func (c *ClientInformation) ClientLeft(p *server.Client) {}

View File

@ -13,7 +13,7 @@ import (
func (g *Graph) Init(game *server.Game) { func (g *Graph) Init(game *server.Game) {
game.AddHandler(&server.PacketHandler{ game.AddHandler(&server.PacketHandler{
ID: packetid.ServerboundChat, 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 var msg pk.String
if err := pk.Packet(packet).Scan(&msg); err != nil { if err := pk.Packet(packet).Scan(&msg); err != nil {
return err return err
@ -33,12 +33,12 @@ func (g *Graph) Init(game *server.Game) {
// Run implement server.Component for Graph // Run implement server.Component for Graph
func (g *Graph) Run(ctx context.Context) {} func (g *Graph) Run(ctx context.Context) {}
// AddPlayer implement server.Component for Graph // ClientJoin implement server.Component for Graph
func (g *Graph) AddPlayer(p *server.Player) { func (g *Graph) ClientJoin(client *server.Client, _ *server.Player) {
p.WritePacket(server.Packet758(pk.Marshal( client.WritePacket(server.Packet758(pk.Marshal(
packetid.ClientboundCommands, g, packetid.ClientboundCommands, g,
))) )))
} }
// RemovePlayer implement server.Component for Graph // ClientLeft implement server.Component for Graph
func (g *Graph) RemovePlayer(p *server.Player) {} func (g *Graph) ClientLeft(_ *server.Client, _ *server.Player) {}

View File

@ -9,8 +9,8 @@ import (
type Level interface { type Level interface {
Init(g *Game) Init(g *Game)
Info() LevelInfo Info() LevelInfo
PlayerJoin(p *Player) PlayerJoin(p *Client)
PlayerQuit(p *Player) PlayerQuit(p *Client)
} }
type LevelInfo struct { 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 { for pos, column := range s.columns {
packet := pk.Marshal( packet := pk.Marshal(
packetid.ClientboundLevelChunkWithLight, packetid.ClientboundLevelChunkWithLight,
@ -53,4 +53,4 @@ func (s *SimpleDim) PlayerJoin(p *Player) {
} }
} }
func (s *SimpleDim) PlayerQuit(*Player) {} func (s *SimpleDim) PlayerQuit(*Client) {}

View File

@ -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() {
}

58
server/ecs/bitest.go Normal file
View File

@ -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)
}
}

44
server/ecs/dispatcher.go Normal file
View File

@ -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()
}

34
server/ecs/ecs_test.go Normal file
View File

@ -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)
}

78
server/ecs/storage.go Normal file
View File

@ -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)
}

66
server/ecs/system.go Normal file
View File

@ -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)
}

13
server/ecs/system_test.go Normal file
View File

@ -0,0 +1,13 @@
package ecs
type PositionComponent struct {
X, Y int
}
type MySystem1 struct {
*PositionComponent
}
func (s *MySystem1) Update(w *World) {
}

90
server/ecs/world.go Normal file
View File

@ -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)
}
}
}

4
server/entity.go Normal file
View File

@ -0,0 +1,4 @@
package server
type Pos struct{ X, Y, Z float64 }
type Rot struct{ Yaw, Pitch float32 }

View File

@ -3,15 +3,13 @@ package server
import ( import (
"context" "context"
_ "embed" _ "embed"
"sync" "time"
"sync/atomic"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/nbt"
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/Tnze/go-mc/server/ecs"
) )
type GamePlay interface { type GamePlay interface {
@ -23,18 +21,10 @@ type GamePlay interface {
} }
type Game struct { type Game struct {
Dim Level *ecs.World
components []Component *ecs.Dispatcher
handlers map[int32][]*PacketHandler handlers map[int32][]*PacketHandler
components []Component
eid int32
}
type Component interface {
Init(g *Game)
Run(ctx context.Context)
AddPlayer(p *Player)
RemovePlayer(p *Player)
} }
type PacketHandler struct { type PacketHandler struct {
@ -42,24 +32,27 @@ type PacketHandler struct {
F packetHandlerFunc F packetHandlerFunc
} }
type packetHandlerFunc func(player *Player, packet Packet758) error type packetHandlerFunc func(client *Client, player *Player, packet Packet758) error
//go:embed DimensionCodec.snbt type Component interface {
var dimensionCodecSNBT nbt.StringifiedMessage Init(g *Game)
Run(ctx context.Context)
ClientJoin(c *Client, p *Player)
ClientLeft(c *Client, p *Player)
}
//go:embed Dimension.snbt func NewGame(components ...Component) *Game {
var dimensionSNBT nbt.StringifiedMessage
func NewGame(dim Level, components ...Component) *Game {
g := &Game{ g := &Game{
Dim: dim, World: ecs.NewWorld(),
components: components, Dispatcher: ecs.NewDispatcher(),
handlers: make(map[int32][]*PacketHandler), handlers: make(map[int32][]*PacketHandler),
components: components,
} }
dim.Init(g) for _, c := range components {
for _, v := range components { c.Init(g)
v.Init(g)
} }
ecs.Register[Client, *ecs.HashMapStorage[Client]](g.World)
ecs.Register[Player, *ecs.HashMapStorage[Player]](g.World)
return g return g
} }
@ -68,73 +61,68 @@ func (g *Game) AddHandler(ph *PacketHandler) {
} }
func (g *Game) Run(ctx context.Context) { func (g *Game) Run(ctx context.Context) {
var wg sync.WaitGroup
wg.Add(len(g.components))
for _, c := range g.components { for _, c := range g.components {
go func(c Component) { go c.Run(ctx)
defer wg.Done() }
c.Run(ctx) ticker := time.NewTicker(time.Second / 20)
}(c) 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) { func (g *Game) AcceptPlayer(name string, id uuid.UUID, protocol int32, conn *net.Conn) {
p := NewPlayer(conn, name, id, g.newEID(), 1) eid := g.CreateEntity(
defer p.Close() Client{
dimInfo := g.Dim.Info() Conn: conn,
err := p.Conn.WritePacket(pk.Marshal( Protocol: protocol,
packetid.ClientboundLogin, packetQueue: NewPacketQueue(),
pk.Int(p.EntityID), // Entity ID errChan: make(chan error, 1),
pk.Boolean(false), // Is hardcore },
pk.Byte(p.Gamemode), // Gamemode Player{
pk.Byte(-1), // Prev Gamemode UUID: id,
pk.Array([]pk.Identifier{ Name: name,
pk.Identifier(dimInfo.Name), },
}), )
pk.NBT(dimensionCodecSNBT), c := ecs.GetComponent[Client](g.World).GetValue(eid)
pk.NBT(dimensionSNBT), p := ecs.GetComponent[Player](g.World).GetValue(eid)
pk.Identifier(dimInfo.Name), // World Name defer c.packetQueue.Close()
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
}
g.Dim.PlayerJoin(p) go func() {
defer g.Dim.PlayerQuit(p) for {
packet, ok := c.packetQueue.Pull()
for _, c := range g.components { if !ok {
c.AddPlayer(p) break
if err := p.GetErr(); err != nil { }
return 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 var packet pk.Packet
for { for {
if err := p.ReadPacket(&packet); err != nil { if err := c.ReadPacket(&packet); err != nil {
return return
} }
for _, ph := range g.handlers[packet.ID] { 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 return
} }
if err := p.GetErr(); err != nil { if err := c.GetErr(); err != nil {
return return
} }
} }
} }
} }
func (g *Game) newEID() int32 {
return atomic.AddInt32(&g.eid, 1)
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 }

View File

@ -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)
})
}

View File

@ -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
}

View File

@ -4,11 +4,11 @@ import (
"container/list" "container/list"
"context" "context"
"errors" "errors"
"github.com/google/uuid"
"time" "time"
"github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/data/packetid"
pk "github.com/Tnze/go-mc/net/packet" 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 // 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 // keepAliveWaitInterval represents how long does the player expired
const keepAliveWaitInterval = time.Second * 30 const keepAliveWaitInterval = time.Second * 30
type ClientDelay struct {
Delay time.Duration
}
type KeepAlive struct { type KeepAlive struct {
join chan *Player join chan *Client
quit chan *Player quit chan *Client
tick chan *Player tick chan *Client
pingList *list.List pingList *list.List
waitList *list.List waitList *list.List
listIndex map[uuid.UUID]*list.Element listIndex map[*Client]*list.Element
listTimer *time.Timer listTimer *time.Timer
waitTimer *time.Timer waitTimer *time.Timer
// The Notchian server uses a system-dependent time in milliseconds to generate the keep alive ID value. // 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. // We don't do that here for security reason.
keepAliveID int64 keepAliveID int64
updatePlayerDelay func(p *Player, delay time.Duration) updatePlayerDelay []func(p *Client, delay time.Duration)
} }
func NewKeepAlive() (k *KeepAlive) { func NewKeepAlive() (k *KeepAlive) {
return &KeepAlive{ return &KeepAlive{
join: make(chan *Player), join: make(chan *Client),
quit: make(chan *Player), quit: make(chan *Client),
tick: make(chan *Player), tick: make(chan *Client),
pingList: list.New(), pingList: list.New(),
waitList: list.New(), waitList: list.New(),
listIndex: make(map[uuid.UUID]*list.Element), listIndex: make(map[*Client]*list.Element),
listTimer: time.NewTimer(keepAliveInterval), listTimer: time.NewTimer(keepAliveInterval),
waitTimer: time.NewTimer(keepAliveWaitInterval), waitTimer: time.NewTimer(keepAliveWaitInterval),
keepAliveID: 0, keepAliveID: 0,
} }
} }
func (k *KeepAlive) AddPlayerDelayUpdateHandler(f func(p *Player, delay time.Duration)) { func (k *KeepAlive) AddPlayerDelayUpdateHandler(f func(p *Client, delay time.Duration)) {
if k.updatePlayerDelay != nil { k.updatePlayerDelay = append(k.updatePlayerDelay, f)
panic("add player update handler twice")
}
k.updatePlayerDelay = f
} }
// Init implement Component for KeepAlive // Init implement Component for KeepAlive
func (k *KeepAlive) Init(g *Game) { 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{ g.AddHandler(&PacketHandler{
ID: packetid.ServerboundKeepAlive, ID: packetid.ServerboundKeepAlive,
F: func(player *Player, packet Packet758) error { F: func(client *Client, player *Player, packet Packet758) error {
var KeepAliveID pk.Long var KeepAliveID pk.Long
if err := pk.Packet(packet).Scan(&KeepAliveID); err != nil { if err := pk.Packet(packet).Scan(&KeepAliveID); err != nil {
return err return err
} }
k.tick <- player k.tick <- client
return nil return nil
}, },
}) })
@ -90,21 +96,21 @@ func (k *KeepAlive) Run(ctx context.Context) {
} }
} }
// AddPlayer implement Component for KeepAlive // ClientJoin implement Component for KeepAlive
func (k *KeepAlive) AddPlayer(player *Player) { k.join <- player } func (k *KeepAlive) ClientJoin(client *Client, _ *Player) { k.join <- client }
// RemovePlayer implement Component for KeepAlive // ClientLeft implement Component for KeepAlive
func (k *KeepAlive) RemovePlayer(p *Player) { k.quit <- p } func (k *KeepAlive) ClientLeft(client *Client, _ *Player) { k.quit <- client }
func (k KeepAlive) pushPlayer(p *Player) { func (k KeepAlive) pushPlayer(p *Client) {
k.listIndex[p.UUID] = k.pingList.PushBack( k.listIndex[p] = k.pingList.PushBack(
keepAliveItem{player: p, t: time.Now()}, keepAliveItem{player: p, t: time.Now()},
) )
} }
func (k *KeepAlive) removePlayer(p *Player) { func (k *KeepAlive) removePlayer(p *Client) {
elem := k.listIndex[p.UUID] elem := k.listIndex[p]
delete(k.listIndex, p.UUID) delete(k.listIndex, p)
if elem.Prev() == nil { if elem.Prev() == nil {
// At present, it is difficult to distinguish // At present, it is difficult to distinguish
// which linked list the player is in, // which linked list the player is in,
@ -126,7 +132,7 @@ func (k *KeepAlive) pingPlayer(now time.Time) {
))) )))
k.keepAliveID++ k.keepAliveID++
// Clientbound KeepAlive packet is sent, move the player to waiting list. // 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}, keepAliveItem{player: p, t: now},
) )
} }
@ -134,10 +140,10 @@ func (k *KeepAlive) pingPlayer(now time.Time) {
keepAliveSetTimer(k.pingList, k.listTimer, keepAliveInterval) keepAliveSetTimer(k.pingList, k.listTimer, keepAliveInterval)
} }
func (k *KeepAlive) tickPlayer(p *Player) { func (k *KeepAlive) tickPlayer(p *Client) {
elem, ok := k.listIndex[p.UUID] elem, ok := k.listIndex[p]
if !ok { 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 return
} }
if elem.Prev() == nil { if elem.Prev() == nil {
@ -147,13 +153,13 @@ func (k *KeepAlive) tickPlayer(p *Player) {
defer keepAliveSetTimer(k.waitList, k.waitTimer, keepAliveWaitInterval) defer keepAliveSetTimer(k.waitList, k.waitTimer, keepAliveWaitInterval)
} }
// update delay of player // update delay of player
t := k.waitList.Remove(elem).(keepAliveItem).t
now := time.Now() now := time.Now()
if k.updatePlayerDelay != nil { delay := now.Sub(k.waitList.Remove(elem).(keepAliveItem).t)
k.updatePlayerDelay(p, now.Sub(t)) for _, f := range k.updatePlayerDelay {
f(p, delay)
} }
// move the player to ping list // move the player to ping list
k.listIndex[p.UUID] = k.pingList.PushBack( k.listIndex[p] = k.pingList.PushBack(
keepAliveItem{player: p, t: now}, keepAliveItem{player: p, t: now},
) )
} }
@ -180,6 +186,6 @@ func keepAliveSetTimer(l *list.List, timer *time.Timer, interval time.Duration)
} }
type keepAliveItem struct { type keepAliveItem struct {
player *Player player *Client
t time.Time t time.Time
} }

View File

@ -4,14 +4,16 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"image"
"image/png"
"strings"
"github.com/google/uuid"
"github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/chat"
"github.com/Tnze/go-mc/data/packetid" "github.com/Tnze/go-mc/data/packetid"
"github.com/Tnze/go-mc/net" "github.com/Tnze/go-mc/net"
pk "github.com/Tnze/go-mc/net/packet" pk "github.com/Tnze/go-mc/net/packet"
"github.com/google/uuid"
"image"
"image/png"
"strings"
) )
// ListPingHandler collect server running status info // ListPingHandler collect server running status info

112
server/player/player.go Normal file
View File

@ -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
},
})
}

163
server/player/playerinfo.go Normal file
View File

@ -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,
// }
//}

37
server/player/pool.go Normal file
View File

@ -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
}

View File

@ -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}
}

View File

@ -16,7 +16,7 @@ import (
// This struct should not be copied after used. // This struct should not be copied after used.
type PlayerList struct { type PlayerList struct {
maxPlayer int maxPlayer int
players map[uuid.UUID]*Player clients map[uuid.UUID]*Player
// Only the field players is protected by this Mutex. // Only the field players is protected by this Mutex.
// Because others field never change after created. // Because others field never change after created.
playersLock sync.Mutex playersLock sync.Mutex
@ -26,7 +26,7 @@ type PlayerList struct {
func NewPlayerList(maxPlayers int) *PlayerList { func NewPlayerList(maxPlayers int) *PlayerList {
return &PlayerList{ return &PlayerList{
maxPlayer: maxPlayers, 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 // Run implement Component for PlayerList
func (p *PlayerList) Run(context.Context) {} func (p *PlayerList) Run(context.Context) {}
// AddPlayer implement Component for PlayerList // ClientJoin implement Component for PlayerList
func (p *PlayerList) AddPlayer(player *Player) { func (p *PlayerList) ClientJoin(client *Client, player *Player) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
if len(p.players) >= p.maxPlayer { if len(p.clients) >= p.maxPlayer {
player.WritePacket(Packet758(pk.Marshal( client.WritePacket(Packet758(pk.Marshal(
packetid.ClientboundDisconnect, packetid.ClientboundDisconnect,
chat.TranslateMsg("multiplayer.disconnect.server_full"), chat.TranslateMsg("multiplayer.disconnect.server_full"),
))) )))
player.PutErr(errors.New("playerlist: server full")) client.PutErr(errors.New("playerlist: server full"))
return return
} }
p.players[player.UUID] = player p.clients[player.UUID] = player
} }
// RemovePlayer implement Component for PlayerList // ClientLeft implement Component for PlayerList
func (p *PlayerList) RemovePlayer(player *Player) { func (p *PlayerList) ClientLeft(_ *Client, player *Player) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
delete(p.players, player.UUID) delete(p.clients, player.UUID)
} }
// CheckPlayer implement LoginChecker for PlayerList // CheckPlayer implement LoginChecker for PlayerList
func (p *PlayerList) CheckPlayer(name string, id uuid.UUID, protocol int32) (ok bool, reason chat.Message) { func (p *PlayerList) CheckPlayer(name string, id uuid.UUID, protocol int32) (ok bool, reason chat.Message) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() 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 false, chat.TranslateMsg("multiplayer.disconnect.server_full")
} }
return true, chat.Message{} return true, chat.Message{}
@ -77,20 +77,20 @@ func (p *PlayerList) MaxPlayer() int {
func (p *PlayerList) OnlinePlayer() int { func (p *PlayerList) OnlinePlayer() int {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
return len(p.players) return len(p.clients)
} }
func (p *PlayerList) PlayerSamples() (sample []PlayerSample) { func (p *PlayerList) PlayerSamples() (sample []PlayerSample) {
p.playersLock.Lock() p.playersLock.Lock()
defer p.playersLock.Unlock() defer p.playersLock.Unlock()
// Up to 10 players can be returned // Up to 10 players can be returned
length := len(p.players) length := len(p.clients)
if length > 10 { if length > 10 {
length = 10 length = 10
} }
sample = make([]PlayerSample, length) sample = make([]PlayerSample, length)
var i int var i int
for _, v := range p.players { for _, v := range p.clients {
sample[i] = PlayerSample{ sample[i] = PlayerSample{
Name: v.Name, Name: v.Name,
ID: v.UUID, ID: v.UUID,

View File

@ -1,4 +1,4 @@
package dimension package world
import ( import (
"fmt" "fmt"

51
server/world/world.go Normal file
View File

@ -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,
})
}