diff --git a/internal/handlers/home.go b/internal/handlers/home.go index 9671e10..f0f119c 100644 --- a/internal/handlers/home.go +++ b/internal/handlers/home.go @@ -1,10 +1,13 @@ package handlers import ( + "context" "fmt" "html/template" "net/http" "sync" + "sync/atomic" + "time" "github.com/gorilla/websocket" "github.com/markbates/goth/gothic" @@ -24,69 +27,178 @@ type PageData struct { Username string } +type Client struct { + conn *websocket.Conn + send chan []byte + hub *hub + ctx context.Context + cancel context.CancelFunc + id uint64 + mu sync.Mutex // Protect concurrent operations +} + type hub struct { - clients map[*websocket.Conn]bool - Broadcast chan []byte - register chan *websocket.Conn - unregister chan *websocket.Conn - mutex sync.RWMutex + clients sync.Map + broadcast chan []byte + register chan *Client + unregister chan *Client + + // Message pool for reusing byte slices + messagePool sync.Pool } var Hub = &hub{ - clients: make(map[*websocket.Conn]bool), - Broadcast: make(chan []byte), - register: make(chan *websocket.Conn), - unregister: make(chan *websocket.Conn), + broadcast: make(chan []byte, 1024), + register: make(chan *Client, 256), + unregister: make(chan *Client, 256), } var upgrader = websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, CheckOrigin: func(r *http.Request) bool { - return true // this should change + return true }, } +var clientIDCounter uint64 + +const ( + writeWait = 10 * time.Second + pongWait = 60 * time.Second + pingPeriod = 54 * time.Second + maxMessageSize = 512 +) + +func init() { + Hub.messagePool.New = func() interface{} { + return make([]byte, 0, 1024) + } +} + func (h *hub) Run() { for { select { - case conn := <-h.register: - h.mutex.Lock() - h.clients[conn] = true - h.mutex.Unlock() - fmt.Printf("Client connected. Total clients: %d\n", len(h.clients)) - - case conn := <-h.unregister: - h.mutex.Lock() - if _, ok := h.clients[conn]; ok { - delete(h.clients, conn) - conn.Close() + case client := <-h.register: + h.clients.Store(client.id, client) + fmt.Printf("Client connected. ID: %d\n", client.id) + + case client := <-h.unregister: + if _, loaded := h.clients.LoadAndDelete(client.id); loaded { + close(client.send) + fmt.Printf("Client disconnected. ID: %d\n", client.id) } - h.mutex.Unlock() - fmt.Printf("Client disconnected. Total clients: %d\n", len(h.clients)) - - case message := <-h.Broadcast: - h.mutex.RLock() - for conn := range h.clients { - if err := conn.WriteMessage(websocket.TextMessage, message); err != nil { - fmt.Printf("Error sending message to client: %v\n", err) - // Remove failed connection - delete(h.clients, conn) - conn.Close() + + case message := <-h.broadcast: + // Single-threaded broadcasting to avoid race conditions + h.clients.Range(func(key, value interface{}) bool { + client := value.(*Client) + + select { + case client.send <- message: + // Message sent successfully + default: + // Client is slow, remove it + h.clients.Delete(client.id) + close(client.send) + fmt.Printf("Slow client removed. ID: %d\n", client.id) } + return true + }) + + // Return message to pool after all clients processed + h.messagePool.Put(message) + } + } +} + +func (c *Client) readPump() { + defer func() { + c.hub.unregister <- c + c.conn.Close() + c.cancel() + }() + + c.conn.SetReadLimit(maxMessageSize) + c.conn.SetReadDeadline(time.Now().Add(pongWait)) + c.conn.SetPongHandler(func(string) error { + c.conn.SetReadDeadline(time.Now().Add(pongWait)) + return nil + }) + + for { + select { + case <-c.ctx.Done(): + return + default: + _, messageBytes, err := c.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + fmt.Printf("WebSocket error for client %d: %v\n", c.id, err) + } + return + } + + // Get a message buffer from the pool + message := c.hub.messagePool.Get().([]byte) + message = message[:len(messageBytes)] + copy(message, messageBytes) + + // Non-blocking broadcast + select { + case c.hub.broadcast <- message: + // Message will be returned to pool by hub.Run() + default: + // Broadcast buffer full, return message to pool and drop + c.hub.messagePool.Put(message) + } + } + } +} + +func (c *Client) writePump() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + c.conn.Close() + }() + + for { + select { + case <-c.ctx.Done(): + return + + case message, ok := <-c.send: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if !ok { + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + // Write the message + if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil { + fmt.Printf("Write error for client %d: %v\n", c.id, err) + return + } + + case <-ticker.C: + c.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return } - h.mutex.RUnlock() } } } func (h *Handler) Home(w http.ResponseWriter, r *http.Request) { session, err := gothic.Store.Get(r, "user-session") - if err != nil { http.Error(w, "Error retrieving session for welcome page", http.StatusInternalServerError) + return } + username, ok := session.Values["user_name"].(string) var pagedata PageData - if ok { pagedata.Username = username } else { @@ -106,21 +218,22 @@ func (h *Handler) WsHandler(w http.ResponseWriter, r *http.Request) { return } - Hub.register <- conn + ctx, cancel := context.WithCancel(context.Background()) + client := &Client{ + conn: conn, + send: make(chan []byte, 256), + hub: Hub, + ctx: ctx, + cancel: cancel, + id: atomic.AddUint64(&clientIDCounter, 1), + } - defer conn.Close() + Hub.register <- client - for { - _, messageBytes, err := conn.ReadMessage() - if err != nil && !websocket.IsCloseError( - err, - websocket.CloseNormalClosure, - websocket.CloseGoingAway, - websocket.CloseNoStatusReceived, - ) { - fmt.Printf("error reading message: %v", err) - } + // Start pumps in separate goroutines + go client.writePump() + go client.readPump() - Hub.Broadcast <- messageBytes - } + // Wait for completion + <-ctx.Done() } diff --git a/static/script.js b/static/script.js index ab24791..46e9cfb 100644 --- a/static/script.js +++ b/static/script.js @@ -34,7 +34,9 @@ window.addEventListener("DOMContentLoaded", function() { } update() { + this._prevY = this.y; this.vx = 0; + if (this.keys.a) { this.vx = -this.speed; this.facing = -1; @@ -59,17 +61,6 @@ window.addEventListener("DOMContentLoaded", function() { this.vy += this.gravity; this.y += this.vy; - const groundY = this.canvas.height; - const totalHeight = 60; - - if (this.y + totalHeight >= groundY) { - this.y = groundY - totalHeight; - this.vy = 0; - this.onGround = true; - } else { - this.onGround = false; - } - this.x = Math.max(20, Math.min(this.canvas.width - 20, this.x)); if (this.action === "punch") { @@ -299,6 +290,54 @@ window.addEventListener("DOMContentLoaded", function() { return null; } + + resolveCollisions(platforms, groundY) { + const totalHeight = 60; + const halfFootW = 12; + const EPS = 0.5; + + if (this._prevY === undefined) { + this._prevY = this.y; + } + + let landedOnPlatform = false; + + // one-way platforms (land only when falling and crossing from above) + if (this.vy >= 0) { + for (const pf of platforms) { + const feetLeft = this.x - halfFootW; + const feetRight = this.x + halfFootW; + const pfLeft = pf.x, pfRight = pf.x + pf.w; + + const horizOverlap = feetRight > pfLeft && feetLeft < pfRight; + const wasAbove = (this._prevY + totalHeight) <= pf.y + EPS; + const nowBelowTop = (this.y + totalHeight) >= pf.y - EPS; + const dropping = this.crouching === true; // hold 's' to drop through + + console.log({ + feetPrev: this._prevY + 60, feetNow: this.y + 60, pfTop: pf.y, + horizOverlap, wasAbove, nowBelowTop, dropping + }); + + if (horizOverlap && wasAbove && nowBelowTop && !dropping) { + this.y = pf.y - totalHeight; + this.vy = 0; + landedOnPlatform = true; + break; + } + } + } + // --- ground after platforms --- + if (!landedOnPlatform && this.y + totalHeight >= groundY) { + this.y = groundY - totalHeight; + this.vy = 0; + this.onGround = true; + } else if (landedOnPlatform) { + this.onGround = true; + } else { + this.onGround = false; + } + } } class Game { @@ -330,6 +369,16 @@ window.addEventListener("DOMContentLoaded", function() { this._reconnectDelay = 1000; this._maxReconnectDelay = 15000; this._reconnectTimer = null; + + this.platform = [ + { x: 120, y: this.canvas.height - 60, w: 240, h: 12 }, + { x: 200, y: this.canvas.height - 120, w: 240, h: 12 }, + { x: 280, y: this.canvas.height - 180, w: 240, h: 12 }, + { x: 460, y: this.canvas.height - 240, w: 240, h: 12 }, + + + + ] } start() { @@ -392,8 +441,15 @@ window.addEventListener("DOMContentLoaded", function() { const playerArray = Object.values(this.players); + this.ctx.fillStyle = "#333" + + for (const p of this.platform) { + this.ctx.fillRect(p.x, p.y, p.w, p.h) + } + playerArray.forEach(player => { player.update(); // Call the update method for each player + player.resolveCollisions(this.platform, this.canvas.height); }); for (let i = 0; i < playerArray.length; i++) { @@ -454,21 +510,16 @@ window.addEventListener("DOMContentLoaded", function() { switch (e.key) { case 'e': case ' ': - me.punch() this._sendAction('e', "keyDown") break; case 'w': - me.jump(); this._sendAction('k', "keyDown") break; case 's': - me.crouching = true; this._sendAction('j', 'keyDown') break; case 'a': case 'd': - if (!me.keys) me.keys = {}; - if (!me.keys[e.key]) me.keys[e.key] = true; this._sendAction(e.key, 'keyDown') break; default: @@ -489,14 +540,10 @@ window.addEventListener("DOMContentLoaded", function() { } if (e.key === 's') { - me.crouching = false; - me.speed = 2; this._sendAction('j', 'keyUp') } if (e.key === 'a' || e.key === 'd') { - if (!me.keys) me.keys = {}; - me.keys[e.key] = false; this._sendAction(e.key, 'keyUp') } @@ -558,7 +605,6 @@ window.addEventListener("DOMContentLoaded", function() { this.ws.onmessage = (e) => { if (!e.data) return; const data = JSON.parse(e.data); - if (data.Id === this.pageData.username) return; this._ensurePlayer(data.Id) const p = this.players[data.Id];