commit 81f5fead953cd184f4b1ffc242f1086841e75875 Author: Smile Rex Date: Sun Jan 11 17:42:17 2026 +0300 add files diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4e0ef13 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module server + +go 1.25.0 + +require github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..25a9fc4 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..cb61870 --- /dev/null +++ b/main.go @@ -0,0 +1,191 @@ +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 +) + +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) + } +}