From d3a19d5066e7f8cbb278d92f67c6f53ba3f93c19 Mon Sep 17 00:00:00 2001 From: Smile Rex Date: Wed, 14 Jan 2026 11:34:03 +0300 Subject: [PATCH 1/3] new files --- controllers/hub.go | 76 ++++++++++++++++++ main.go | 187 +-------------------------------------------- models/message.go | 6 ++ models/player.go | 19 +++++ 4 files changed, 105 insertions(+), 183 deletions(-) create mode 100644 controllers/hub.go create mode 100644 models/message.go create mode 100644 models/player.go diff --git a/controllers/hub.go b/controllers/hub.go new file mode 100644 index 0000000..0e64d8c --- /dev/null +++ b/controllers/hub.go @@ -0,0 +1,76 @@ +package controllers + +import ( + "bytes" + "encoding/binary" + "log" + "net/http" + "server/models" + + "github.com/gorilla/websocket" +) + +type Hub struct { + Players map[uint32]*models.Player + ServerBuffer []byte +} + +func NewHub() *Hub { + return &Hub{ + Players: make(map[uint32]*models.Player), + } +} + +func (h *Hub) readLoop(conn *websocket.Conn) { + p := &models.Player{ + Conn: conn, + InputX: 0, + InputZ: 0, + } + log.Println("Player connected:", p.ID) + + h.Players[p.ID] = p + + defer func() { + delete(h.Players, p.ID) + + p.Conn.Close() + log.Println("Player disconnected:", p.ID) + }() + + for { + _, data, err := p.Conn.ReadMessage() + if err != nil { + return + } + + if len(data) < 8 { + continue + } + + buf := bytes.NewReader(data) + binary.Read(buf, binary.LittleEndian, &p.InputX) + binary.Read(buf, binary.LittleEndian, &p.InputZ) + } +} + +func (h *Hub) StartWS(w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } + + for { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + continue + } + + go h.readLoop(conn) + } +} + +func (h *Hub) Start() { + http.HandleFunc("/ws", h.StartWS) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/main.go b/main.go index cb61870..38b5d4e 100644 --- a/main.go +++ b/main.go @@ -1,191 +1,12 @@ package main import ( - "bytes" - "encoding/binary" "log" - "math/rand" - "net/http" - "sync" - "time" - - "github.com/gorilla/websocket" -) - -const ( - PacketHandshake uint8 = 0 - PacketWorld uint8 = 1 - - TickRate = 30 - Speed = 5.0 - - SpawnMinX = -5.0 - SpawnMaxX = 5.0 - SpawnMinZ = -5.0 - SpawnMaxZ = 5.0 - SpawnRadius = 1.5 -) - -type Player struct { - ID uint32 - - X float32 - Y float32 - Z float32 - - InputX float32 - InputZ float32 - - Conn *websocket.Conn -} - -var ( - upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, - } - - players = make(map[uint32]*Player) - playersMu sync.Mutex - nextID uint32 = 1 + "server/controllers" ) func main() { - rand.Seed(time.Now().UnixNano()) - - http.HandleFunc("/ws", handleWS) - - go gameLoop() - - log.Println("Server running on :8080") - log.Fatal(http.ListenAndServe(":8080", nil)) -} - -func handleWS(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - return - } - - playersMu.Lock() - id := nextID - nextID++ - - x, z := randomSpawnPosition() - - player := &Player{ - ID: id, - X: x, - Y: 0, - Z: z, - Conn: conn, - } - players[id] = player - playersMu.Unlock() - - { - buf := new(bytes.Buffer) - binary.Write(buf, binary.LittleEndian, PacketHandshake) - binary.Write(buf, binary.LittleEndian, id) - conn.WriteMessage(websocket.BinaryMessage, buf.Bytes()) - } - - log.Println("Player connected:", id, "spawn at", x, z) - - go readLoop(player) -} - -func randomSpawnPosition() (float32, float32) { - for i := 0; i < 20; i++ { - x := rand.Float32()*(SpawnMaxX-SpawnMinX) + SpawnMinX - z := rand.Float32()*(SpawnMaxZ-SpawnMinZ) + SpawnMinZ - - if isSpawnFree(x, z) { - return x, z - } - } - - // fallback - return 0, 0 -} - -func isSpawnFree(x, z float32) bool { - for _, p := range players { - dx := p.X - x - dz := p.Z - z - if dx*dx+dz*dz < SpawnRadius*SpawnRadius { - return false - } - } - return true -} - -func readLoop(p *Player) { - defer func() { - playersMu.Lock() - delete(players, p.ID) - playersMu.Unlock() - - p.Conn.Close() - log.Println("Player disconnected:", p.ID) - }() - - for { - _, data, err := p.Conn.ReadMessage() - if err != nil { - return - } - - if len(data) < 8 { - continue - } - - buf := bytes.NewReader(data) - binary.Read(buf, binary.LittleEndian, &p.InputX) - binary.Read(buf, binary.LittleEndian, &p.InputZ) - } -} - -func gameLoop() { - ticker := time.NewTicker(time.Second / TickRate) - defer ticker.Stop() - - dt := float32(1.0 / TickRate) - - for range ticker.C { - update(dt) - broadcast() - } -} - -func update(dt float32) { - playersMu.Lock() - defer playersMu.Unlock() - - for _, p := range players { - p.X += p.InputX * Speed * dt - p.Z += p.InputZ * Speed * dt - } -} - -func broadcast() { - playersMu.Lock() - defer playersMu.Unlock() - - buf := new(bytes.Buffer) - - binary.Write(buf, binary.LittleEndian, PacketWorld) - binary.Write(buf, binary.LittleEndian, uint16(len(players))) - - for _, p := range players { - binary.Write(buf, binary.LittleEndian, p.ID) - binary.Write(buf, binary.LittleEndian, p.X) - binary.Write(buf, binary.LittleEndian, p.Y) - binary.Write(buf, binary.LittleEndian, p.Z) - } - - data := buf.Bytes() - - for _, p := range players { - p.Conn.WriteMessage(websocket.BinaryMessage, data) - } + hub := controllers.NewHub() + log.Println("Server listening websocket on :8080") + hub.Start() } diff --git a/models/message.go b/models/message.go new file mode 100644 index 0000000..ba0052d --- /dev/null +++ b/models/message.go @@ -0,0 +1,6 @@ +package models + +type Message struct { + Type int8 + Payload interface{} +} diff --git a/models/player.go b/models/player.go new file mode 100644 index 0000000..997063a --- /dev/null +++ b/models/player.go @@ -0,0 +1,19 @@ +package models + +import "github.com/gorilla/websocket" + +type Player struct { + ID uint32 + + X float32 + Y float32 + Z float32 + + InputX float32 + InputZ float32 + + Name string + PhotoURL string + + Conn *websocket.Conn +} From 83f356cea9dc276fa321f9e69f754ffbe1966ca1 Mon Sep 17 00:00:00 2001 From: Smile Rex Date: Wed, 14 Jan 2026 12:38:03 +0300 Subject: [PATCH 2/3] f --- controllers/hub.go | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/controllers/hub.go b/controllers/hub.go index 0e64d8c..d8dbc7f 100644 --- a/controllers/hub.go +++ b/controllers/hub.go @@ -2,7 +2,6 @@ package controllers import ( "bytes" - "encoding/binary" "log" "net/http" "server/models" @@ -23,10 +22,12 @@ func NewHub() *Hub { func (h *Hub) readLoop(conn *websocket.Conn) { p := &models.Player{ + ID: uint32(len(h.Players) + 1), Conn: conn, InputX: 0, InputZ: 0, } + log.Println("Player connected:", p.ID) h.Players[p.ID] = p @@ -49,28 +50,25 @@ func (h *Hub) readLoop(conn *websocket.Conn) { } buf := bytes.NewReader(data) - binary.Read(buf, binary.LittleEndian, &p.InputX) - binary.Read(buf, binary.LittleEndian, &p.InputZ) + log.Println(buf) } } -func (h *Hub) StartWS(w http.ResponseWriter, r *http.Request) { +func (h *Hub) ws(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } - for { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Println(err) - continue - } - - go h.readLoop(conn) + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return } + + go h.readLoop(conn) } func (h *Hub) Start() { - http.HandleFunc("/ws", h.StartWS) + http.HandleFunc("/ws", h.ws) log.Fatal(http.ListenAndServe(":8080", nil)) } From c1df49fde195475cd44c88950c2aed97fa09dc26 Mon Sep 17 00:00:00 2001 From: Smile Rex Date: Wed, 14 Jan 2026 23:10:23 +0300 Subject: [PATCH 3/3] new server --- controllers/connection.go | 55 +++++++++++++++++++++++++++++++++++++ controllers/hub.go | 58 ++++----------------------------------- controllers/players.go | 43 +++++++++++++++++++++++++++++ controllers/protocol.go | 7 +++++ controllers/world.go | 46 +++++++++++++++++++++++++++++++ controllers/ws.go | 22 +++++++++++++++ main.go | 2 -- models/entity.go | 19 +++++++++++++ models/message.go | 46 +++++++++++++++++++++++++++++-- models/player.go | 14 ++-------- models/reader.go | 50 +++++++++++++++++++++++++++++++++ models/writer.go | 49 +++++++++++++++++++++++++++++++++ 12 files changed, 342 insertions(+), 69 deletions(-) create mode 100644 controllers/connection.go create mode 100644 controllers/players.go create mode 100644 controllers/protocol.go create mode 100644 controllers/world.go create mode 100644 controllers/ws.go create mode 100644 models/entity.go create mode 100644 models/reader.go create mode 100644 models/writer.go diff --git a/controllers/connection.go b/controllers/connection.go new file mode 100644 index 0000000..74265b7 --- /dev/null +++ b/controllers/connection.go @@ -0,0 +1,55 @@ +package controllers + +import ( + "log" + "server/models" + + "github.com/gorilla/websocket" +) + +func (h *Hub) readLoop(conn *websocket.Conn) { + handMessage := models.Message{Type: MSG_WELCOME} + _ = conn.WriteMessage(websocket.BinaryMessage, handMessage.Encode()) + + var player *models.Player + + defer func() { + if player != nil { + h.removePlayer(player.ID) + log.Println("Player left:", player.ID) + } + conn.Close() + }() + + for { + _, bytes, err := conn.ReadMessage() + if err != nil { + return + } + + msg, err := models.Decode(bytes) + if err != nil { + log.Println(err) + continue + } + + switch msg.Type { + + case MSG_WELCOME: + if player != nil { + continue + } + player = h.handShake(msg.Payload, conn) + log.Println("Player joined:", player.ID) + + case MSG_INPUT: + if player == nil { + continue + } + reader := models.NewReader(msg.Payload) + x := reader.ReadF32() + y := reader.ReadF32() + h.updatePosition(x, y, player) + } + } +} diff --git a/controllers/hub.go b/controllers/hub.go index d8dbc7f..396e709 100644 --- a/controllers/hub.go +++ b/controllers/hub.go @@ -1,17 +1,15 @@ package controllers import ( - "bytes" "log" "net/http" "server/models" - - "github.com/gorilla/websocket" + "sync" ) type Hub struct { - Players map[uint32]*models.Player - ServerBuffer []byte + Players map[uint32]*models.Player + Mu sync.RWMutex } func NewHub() *Hub { @@ -20,55 +18,9 @@ func NewHub() *Hub { } } -func (h *Hub) readLoop(conn *websocket.Conn) { - p := &models.Player{ - ID: uint32(len(h.Players) + 1), - Conn: conn, - InputX: 0, - InputZ: 0, - } - - log.Println("Player connected:", p.ID) - - h.Players[p.ID] = p - - defer func() { - delete(h.Players, p.ID) - - p.Conn.Close() - log.Println("Player disconnected:", p.ID) - }() - - for { - _, data, err := p.Conn.ReadMessage() - if err != nil { - return - } - - if len(data) < 8 { - continue - } - - buf := bytes.NewReader(data) - log.Println(buf) - } -} - -func (h *Hub) ws(w http.ResponseWriter, r *http.Request) { - upgrader := websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, - } - - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - log.Println(err) - return - } - - go h.readLoop(conn) -} - func (h *Hub) Start() { + go h.updateWorld() http.HandleFunc("/ws", h.ws) + log.Println("Server listen port 8080") log.Fatal(http.ListenAndServe(":8080", nil)) } diff --git a/controllers/players.go b/controllers/players.go new file mode 100644 index 0000000..74bffbc --- /dev/null +++ b/controllers/players.go @@ -0,0 +1,43 @@ +package controllers + +import ( + "server/models" + + "github.com/gorilla/websocket" +) + +func (h *Hub) handShake(msg []byte, conn *websocket.Conn) *models.Player { + reader := models.NewReader(msg) + newID := reader.ReadU32() + name := reader.ReadString() + + player := &models.Player{ + Entity: models.Entity{ + Type: models.EntityPlayer, + ID: newID, + }, + Name: name, + Conn: conn, + } + + h.Mu.Lock() + h.Players[player.ID] = player + h.Mu.Unlock() + + return player +} + +func (h *Hub) updatePosition(x, y float32, player *models.Player) { + h.Mu.Lock() + defer h.Mu.Unlock() + + player.X = x + player.Y = y +} + +func (h *Hub) removePlayer(id uint32) { + h.Mu.Lock() + defer h.Mu.Unlock() + + delete(h.Players, id) +} diff --git a/controllers/protocol.go b/controllers/protocol.go new file mode 100644 index 0000000..92d1a59 --- /dev/null +++ b/controllers/protocol.go @@ -0,0 +1,7 @@ +package controllers + +const ( + MSG_WELCOME = 0 + MSG_INPUT = 1 + MSG_SNAPSHOT = 2 +) diff --git a/controllers/world.go b/controllers/world.go new file mode 100644 index 0000000..fdb014e --- /dev/null +++ b/controllers/world.go @@ -0,0 +1,46 @@ +package controllers + +import ( + "server/models" + "time" + + "github.com/gorilla/websocket" +) + +func (h *Hub) broadcastSnapshot() { + h.Mu.RLock() + defer h.Mu.RUnlock() + + w := models.Writer{} + w.WriteU16(uint16(len(h.Players))) + + for _, p := range h.Players { + w.WriteU32(p.ID) + w.WriteU8(uint8(models.EntityPlayer)) + w.WriteF32(p.X) + w.WriteF32(p.Y) + w.WriteF32(p.Z) + w.WriteF32(p.Yaw) + } + + msg := models.Message{ + Type: MSG_SNAPSHOT, + Version: 1, + Payload: w.Bytes(), + } + + for _, p := range h.Players { + if p.Conn != nil { + _ = p.Conn.WriteMessage(websocket.BinaryMessage, msg.Encode()) + } + } +} + +func (h *Hub) updateWorld() { + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + + for range ticker.C { + h.broadcastSnapshot() + } +} diff --git a/controllers/ws.go b/controllers/ws.go new file mode 100644 index 0000000..06508de --- /dev/null +++ b/controllers/ws.go @@ -0,0 +1,22 @@ +package controllers + +import ( + "log" + "net/http" + + "github.com/gorilla/websocket" +) + +func (h *Hub) ws(w http.ResponseWriter, r *http.Request) { + upgrader := websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + go h.readLoop(conn) +} diff --git a/main.go b/main.go index 38b5d4e..8c9ed92 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,10 @@ package main import ( - "log" "server/controllers" ) func main() { hub := controllers.NewHub() - log.Println("Server listening websocket on :8080") hub.Start() } diff --git a/models/entity.go b/models/entity.go new file mode 100644 index 0000000..4f99584 --- /dev/null +++ b/models/entity.go @@ -0,0 +1,19 @@ +package models + +type EntityType uint8 + +const ( + EntityPlayer EntityType = 1 + EntityNPC EntityType = 2 + EntityBullet EntityType = 3 +) + +type Entity struct { + ID uint32 + Type EntityType + + X float32 + Y float32 + Z float32 + Yaw float32 +} diff --git a/models/message.go b/models/message.go index ba0052d..afb9a4e 100644 --- a/models/message.go +++ b/models/message.go @@ -1,6 +1,48 @@ package models +import ( + "encoding/binary" + "errors" +) + type Message struct { - Type int8 - Payload interface{} + Type uint16 + Version uint16 + Payload []byte +} + +func (m *Message) Encode() []byte { + payloadLen := len(m.Payload) + if payloadLen > 0xFFFF { + return nil + } + + buf := make([]byte, 8+payloadLen) + + binary.LittleEndian.PutUint16(buf[0:2], m.Type) + binary.LittleEndian.PutUint16(buf[2:4], m.Version) + binary.LittleEndian.PutUint16(buf[4:6], uint16(payloadLen)) + + copy(buf[8:], m.Payload) + return buf +} + +func Decode(data []byte) (*Message, error) { + if len(data) < 8 { + return nil, errors.New("message too short") + } + + payloadLen := binary.LittleEndian.Uint16(data[4:6]) + if len(data) != int(8+payloadLen) { + return nil, errors.New("invalid payload length") + } + + msg := &Message{ + Type: binary.LittleEndian.Uint16(data[0:2]), + Version: binary.LittleEndian.Uint16(data[2:4]), + Payload: make([]byte, payloadLen), + } + + copy(msg.Payload, data[8:]) + return msg, nil } diff --git a/models/player.go b/models/player.go index 997063a..66c6060 100644 --- a/models/player.go +++ b/models/player.go @@ -3,17 +3,7 @@ package models import "github.com/gorilla/websocket" type Player struct { - ID uint32 - - X float32 - Y float32 - Z float32 - - InputX float32 - InputZ float32 - - Name string - PhotoURL string - + Entity + Name string Conn *websocket.Conn } diff --git a/models/reader.go b/models/reader.go new file mode 100644 index 0000000..a388a72 --- /dev/null +++ b/models/reader.go @@ -0,0 +1,50 @@ +package models + +import ( + "encoding/binary" + "math" +) + +type Reader struct { + buf []byte + off int +} + +func NewReader(b []byte) *Reader { + return &Reader{buf: b} +} + +func (r *Reader) ReadU8() uint8 { + v := r.buf[r.off] + r.off++ + return v +} + +func (r *Reader) ReadU16() uint16 { + v := binary.LittleEndian.Uint16(r.buf[r.off:]) + r.off += 2 + return v +} + +func (r *Reader) ReadU32() uint32 { + v := binary.LittleEndian.Uint32(r.buf[r.off:]) + r.off += 4 + return v +} + +func (r *Reader) ReadF32() float32 { + v := math.Float32frombits(binary.LittleEndian.Uint32(r.buf[r.off:])) + r.off += 4 + return v +} + +func (r *Reader) ReadBool() bool { + return r.ReadU8() == 1 +} + +func (r *Reader) ReadString() string { + l := r.ReadU16() + s := string(r.buf[r.off : r.off+int(l)]) + r.off += int(l) + return s +} diff --git a/models/writer.go b/models/writer.go new file mode 100644 index 0000000..4b2a2f1 --- /dev/null +++ b/models/writer.go @@ -0,0 +1,49 @@ +package models + +import ( + "encoding/binary" + "math" +) + +type Writer struct { + buf []byte +} + +func (w *Writer) Bytes() []byte { + return w.buf +} + +func (w *Writer) WriteU8(v uint8) { + w.buf = append(w.buf, v) +} + +func (w *Writer) WriteU16(v uint16) { + tmp := make([]byte, 2) + binary.LittleEndian.PutUint16(tmp, v) + w.buf = append(w.buf, tmp...) +} + +func (w *Writer) WriteU32(v uint32) { + tmp := make([]byte, 4) + binary.LittleEndian.PutUint32(tmp, v) + w.buf = append(w.buf, tmp...) +} + +func (w *Writer) WriteF32(v float32) { + tmp := make([]byte, 4) + binary.LittleEndian.PutUint32(tmp, math.Float32bits(v)) + w.buf = append(w.buf, tmp...) +} + +func (w *Writer) WriteBool(v bool) { + if v { + w.WriteU8(1) + } else { + w.WriteU8(0) + } +} + +func (w *Writer) WriteString(s string) { + w.WriteU16(uint16(len(s))) + w.buf = append(w.buf, []byte(s)...) +}