From 81159196b1a226f1cb825a32d89c741f013e2e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Sat, 15 Aug 2026 09:14:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=8A=9F=E8=83=BD=E3=80=81?= =?UTF-8?q?=E5=88=B7=E6=96=B0=E4=B8=8D=E7=AB=8B=E5=8D=B3=E8=A7=A3=E6=95=A3?= =?UTF-8?q?=E6=88=BF=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/room/hub.go | 27 +++++++++++++++++++++++++++ internal/room/room.go | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/internal/room/hub.go b/internal/room/hub.go index e1b456a..bd9c2f8 100644 --- a/internal/room/hub.go +++ b/internal/room/hub.go @@ -319,3 +319,30 @@ func (h *Hub) removeUserRoom(uid int) { delete(h.userRoom, uid) h.mu.Unlock() } + +// onStarveOfflineTimeout 饥荒等待中断线保护超时:30秒内未重连才清座;若房间无真人则解散 +func (h *Hub) onStarveOfflineTimeout(r *Room, userID, seatIndex int) { + h.mu.Lock() + defer h.mu.Unlock() + r.mu.Lock() + defer r.mu.Unlock() + + seat := r.seatByUser(userID) + if seat == nil || seat.Index != seatIndex || seat.Online || seat.client != nil || r.closed { + return + } + r.clearSeatLocked(seat) + r.broadcastStateLocked() + delete(h.userRoom, userID) + + empty := true + for _, s := range r.seats { + if s.UserID > 0 { + empty = false + break + } + } + if empty { + delete(h.rooms, r.Code) + } +} diff --git a/internal/room/room.go b/internal/room/room.go index 3f189d2..11c97fc 100644 --- a/internal/room/room.go +++ b/internal/room/room.go @@ -31,16 +31,17 @@ const ( // Seat 房间内的一个座位 type Seat struct { - Index int // 座位号(斗地主0-2,象棋0红1黑) - UserID int // 真人用户ID(AI 座位为 0) - Name string // 显示昵称 - Avatar string // 头像 - Skin string // 皮肤编码(饥荒组队联机:更衣室选择的皮肤) - IsAI bool // 是否 AI 座位 - Ready bool // 是否已准备(AI 恒为 true) - Online bool // 是否在线(断线重连用) - Auto bool // 真人主动托管中(每回合由规则 AI 代打,结算后自动取消) - client *Client // 关联的连接(AI 为 nil) + Index int // 座位号(斗地主0-2,象棋0红1黑) + UserID int // 真人用户ID(AI 座位为 0) + Name string // 显示昵称 + Avatar string // 头像 + Skin string // 皮肤编码(饥荒组队联机:更衣室选择的皮肤) + IsAI bool // 是否 AI 座位 + Ready bool // 是否已准备(AI 恒为 true) + Online bool // 是否在线(断线重连用) + Auto bool // 真人主动托管中(每回合由规则 AI 代打,结算后自动取消) + client *Client // 关联的连接(AI 为 nil) + offlineTimer *time.Timer // 饥荒等待中断线保护:30秒后仍未重连才清座 } // chessState 象棋对局状态 @@ -93,6 +94,7 @@ type Room struct { // 同一好友再次邀请的最短间隔 const inviteCooldown = 60 * time.Second +const starveOfflineGrace = 30 * time.Second // 饥荒等待中断线保护:30秒内重连不散房 // newRoom 构造房间:按游戏类型确定座位数,按配置构造 AI 实例 func newRoom(code, game, mode, aiProvider, aiDifficulty string) *Room { @@ -202,6 +204,10 @@ func (r *Room) reattach(c *Client) { defer r.mu.Unlock() for _, s := range r.seats { if s.UserID == c.userID { + if s.offlineTimer != nil { + s.offlineTimer.Stop() + s.offlineTimer = nil + } s.client = c s.Online = true r.touch() @@ -223,6 +229,18 @@ func (r *Room) onOffline(c *Client) (leftRoom bool) { seat.client = nil seat.Online = false if r.status == "waiting" { + if r.Game == "starve" { + // 饥荒等待中断线保护:保留座位 30 秒,刷新/断线重连成功后取消清理 + if seat.offlineTimer != nil { + seat.offlineTimer.Stop() + } + offlineUID, offlineSeat := seat.UserID, seat.Index + seat.offlineTimer = time.AfterFunc(starveOfflineGrace, func() { + c.hub.onStarveOfflineTimeout(r, offlineUID, offlineSeat) + }) + r.broadcastStateLocked() + return false + } r.clearSeatLocked(seat) r.broadcastStateLocked() return true @@ -301,6 +319,10 @@ func (r *Room) leave(c *Client) { // clearSeatLocked 清空座位;房主离开时移交房主(需持锁) func (r *Room) clearSeatLocked(seat *Seat) { uid := seat.UserID + if seat.offlineTimer != nil { + seat.offlineTimer.Stop() + seat.offlineTimer = nil + } *seat = Seat{Index: seat.Index} if uid == r.hostUserID { r.hostUserID = 0