605 lines
20 KiB
Go
Executable File
605 lines
20 KiB
Go
Executable File
package handlers
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math/rand"
|
||
"time"
|
||
|
||
"wuziqi-server/characters"
|
||
"wuziqi-server/config"
|
||
"wuziqi-server/core"
|
||
"wuziqi-server/items"
|
||
"wuziqi-server/logic"
|
||
|
||
"github.com/heroiclabs/nakama-common/runtime"
|
||
)
|
||
|
||
type MatchLabel struct {
|
||
Open bool `json:"open"`
|
||
Started bool `json:"started"`
|
||
PlayerCount int `json:"player_count"`
|
||
MaxPlayers int `json:"max_players"`
|
||
Label string `json:"label"`
|
||
GameType string `json:"game_type"` // 游戏类型标识(minesweeper/minesweeper_free)
|
||
}
|
||
|
||
type MatchHandler struct{}
|
||
|
||
func (m *MatchHandler) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) {
|
||
logger.Info("MatchInit (Refactored) called")
|
||
|
||
// 从 params 中提取 game_type
|
||
gameType := "minesweeper" // 默认值
|
||
if params != nil {
|
||
if gt, ok := params["game_type"]; ok {
|
||
if gtStr, ok := gt.(string); ok && gtStr != "" {
|
||
gameType = gtStr
|
||
}
|
||
}
|
||
}
|
||
logger.Info("MatchInit: GameType = %s", gameType)
|
||
|
||
// 1. 加载配置
|
||
apiConfig := config.GetMinesweeperConfig(logger)
|
||
|
||
// 默认值
|
||
gridSizeSide := 10
|
||
bombCount := 30
|
||
itemMin := 5
|
||
itemMax := 10
|
||
hpInit := 4
|
||
matchPlayerCount := core.MaxPlayers
|
||
enabledItems := make(map[string]bool)
|
||
itemWeights := make(map[string]int)
|
||
charHPConfig := make(map[string]int)
|
||
turnDuration := 15
|
||
|
||
if apiConfig != nil {
|
||
if apiConfig.GridSize > 0 {
|
||
gridSizeSide = apiConfig.GridSize
|
||
}
|
||
if apiConfig.BombCount > 0 {
|
||
bombCount = apiConfig.BombCount
|
||
}
|
||
if apiConfig.ItemMin >= 0 {
|
||
itemMin = apiConfig.ItemMin
|
||
}
|
||
if apiConfig.ItemMax >= itemMin {
|
||
itemMax = apiConfig.ItemMax
|
||
}
|
||
if apiConfig.HPInit > 0 {
|
||
hpInit = apiConfig.HPInit
|
||
}
|
||
if apiConfig.MatchPlayerCount >= 2 {
|
||
matchPlayerCount = apiConfig.MatchPlayerCount
|
||
}
|
||
enabledItems = apiConfig.EnabledItems
|
||
itemWeights = apiConfig.ItemWeights
|
||
charHPConfig = apiConfig.CharacterHP
|
||
if apiConfig.TurnDuration > 0 {
|
||
turnDuration = apiConfig.TurnDuration
|
||
}
|
||
}
|
||
|
||
// 强制最小值检查 (防止配置为0导致死循环)
|
||
if turnDuration < 5 {
|
||
logger.Warn("TurnDuration too small (%d), resetting to 15", turnDuration)
|
||
turnDuration = 15
|
||
}
|
||
|
||
// 2. 初始化管理器
|
||
charMgr := characters.NewCharacterManager(charHPConfig)
|
||
itemMgr := items.NewItemManager()
|
||
|
||
// 3. 生成网格
|
||
// 如果池需要回退,我们需要将所有道具类型传递给 GenerateGrid
|
||
// 道具已在 ItemManager 中注册,但我们需要ID字符串列表
|
||
// 我们在这里硬编码列表或从 items 包中公开它。
|
||
// 目前,硬编码以匹配以前的逻辑。
|
||
allItemTypes := []string{
|
||
"medkit", "bomb_timer", "poison", "shield", "skip",
|
||
"magnifier", "knife", "revive", "lightning", "chest", "curse",
|
||
}
|
||
|
||
grid := logic.GenerateGrid(gridSizeSide, bombCount, itemMin, itemMax, enabledItems, itemWeights, allItemTypes)
|
||
|
||
// 4. 创建初始状态
|
||
gameState := &core.GameState{
|
||
Players: make(map[string]*core.Player),
|
||
Grid: grid,
|
||
GridSize: gridSizeSide,
|
||
TurnOrder: make([]string, 0),
|
||
CurrentTurnIndex: 0,
|
||
Round: 1,
|
||
WinnerID: "",
|
||
GameStarted: false,
|
||
TurnDuration: turnDuration,
|
||
}
|
||
logger.Info("MatchInit: Configured TurnDuration: %d (API Config: %v)", turnDuration, apiConfig != nil)
|
||
|
||
// 5. 创建比赛状态
|
||
// 我们需要一个持有引擎的简化 MatchState 吗?
|
||
// 或者应该在 MatchLoop 中创建 logic.GameEngine?
|
||
// Nakama 在调用之间传递 'state' interface{}。
|
||
// 我们应该存储 GameState + 引擎依赖项。
|
||
|
||
// 初始化角色池
|
||
charTypes := []string{"elephant", "cat", "dog", "monkey", "chicken", "sloth", "hippo", "tiger"}
|
||
shuffledChars := make([]string, len(charTypes))
|
||
copy(shuffledChars, charTypes)
|
||
rand.Shuffle(len(shuffledChars), func(i, j int) {
|
||
shuffledChars[i], shuffledChars[j] = shuffledChars[j], shuffledChars[i]
|
||
})
|
||
|
||
matchState := &MatchState{
|
||
State: gameState,
|
||
HPInit: hpInit,
|
||
MatchPlayerCount: matchPlayerCount,
|
||
ValidatedPlayers: make(map[string]*config.GameTokenInfo),
|
||
DisconnectedPlayers: make(map[string]*core.Player),
|
||
CharacterPool: shuffledChars,
|
||
CharacterIndex: 0,
|
||
Spectators: make(map[string]bool),
|
||
Presences: make(map[string]runtime.Presence),
|
||
CharManager: charMgr,
|
||
ItemManager: itemMgr,
|
||
GameType: gameType, // 存储房间的游戏类型
|
||
CreatedAt: time.Now().Unix(), // 记录房间创建时间,用于兜底销毁
|
||
}
|
||
|
||
tickRate := 10
|
||
label := "Animal Minesweeper"
|
||
|
||
// 初始化 Label
|
||
initialLabel := MatchLabel{
|
||
Open: true,
|
||
Started: false,
|
||
PlayerCount: 0,
|
||
MaxPlayers: matchPlayerCount,
|
||
Label: label,
|
||
GameType: gameType, // 添加游戏类型到标签
|
||
}
|
||
labelBytes, _ := json.Marshal(initialLabel)
|
||
|
||
return matchState, tickRate, string(labelBytes)
|
||
}
|
||
|
||
type MatchState struct {
|
||
State *core.GameState
|
||
HPInit int
|
||
MatchPlayerCount int
|
||
ValidatedPlayers map[string]*config.GameTokenInfo
|
||
DisconnectedPlayers map[string]*core.Player
|
||
CharacterPool []string
|
||
CharacterIndex int
|
||
Spectators map[string]bool
|
||
Presences map[string]runtime.Presence
|
||
GameType string // 房间的游戏类型(minesweeper/minesweeper_free)
|
||
CreatedAt int64 // 房间创建时间(Unix 秒),用于兜底销毁
|
||
|
||
// 用于构建引擎的管理器
|
||
CharManager *characters.CharacterManager
|
||
ItemManager *items.ItemManager
|
||
}
|
||
|
||
// ... MatchJoinAttempt, MatchJoin, MatchLoop ...
|
||
|
||
func (m *MatchHandler) MatchJoinAttempt(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presence runtime.Presence, metadata map[string]string) (interface{}, bool, string) {
|
||
ms := state.(*MatchState)
|
||
|
||
// Token 验证逻辑
|
||
gameToken, ok := metadata["game_token"]
|
||
if !ok || gameToken == "" {
|
||
logger.Warn("MatchJoinAttempt: No game_token provided for user %s (SID: %s). Metadata: %v", presence.GetUserId(), presence.GetSessionId(), metadata)
|
||
return ms, false, "Game token required"
|
||
}
|
||
|
||
logger.Info("MatchJoinAttempt: Validating token for user %s", presence.GetUserId())
|
||
tokenInfo := config.ValidateGameToken(logger, gameToken)
|
||
if tokenInfo == nil {
|
||
logger.Error("MatchJoinAttempt: Invalid game token for user %s", presence.GetUserId())
|
||
return ms, false, "Invalid game token"
|
||
}
|
||
|
||
// 校验玩家的游戏类型是否与房间匹配
|
||
if tokenInfo.GameType != ms.GameType {
|
||
logger.Error("MatchJoinAttempt: GameType MISMATCH for user %s. Player: %s, Room: %s. This indicates matchmaker query filtering may not be working correctly.",
|
||
presence.GetUserId(), tokenInfo.GameType, ms.GameType)
|
||
return ms, false, fmt.Sprintf("Game type mismatch (player: %s, room: %s)", tokenInfo.GameType, ms.GameType)
|
||
}
|
||
|
||
ms.ValidatedPlayers[presence.GetUserId()] = tokenInfo
|
||
|
||
if ms.State.GameStarted {
|
||
if _, ok := ms.DisconnectedPlayers[presence.GetUserId()]; ok {
|
||
return ms, true, "" // 重连
|
||
}
|
||
ms.Spectators[presence.GetUserId()] = true
|
||
return ms, true, "" // 观众
|
||
}
|
||
|
||
if len(ms.State.Players) >= ms.MatchPlayerCount {
|
||
return ms, false, "Match full"
|
||
}
|
||
|
||
return ms, true, ""
|
||
}
|
||
|
||
func (m *MatchHandler) MatchJoin(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
|
||
ms := state.(*MatchState)
|
||
|
||
// 仅在需要时构建引擎?还是只使用它的组件?
|
||
// 如果需要,我们可以在本地构建它以使用其逻辑,但 Join 逻辑主要是设置。
|
||
|
||
matchID, _ := ctx.Value(runtime.RUNTIME_CTX_MATCH_ID).(string)
|
||
|
||
for _, presence := range presences {
|
||
userID := presence.GetUserId()
|
||
ms.Presences[userID] = presence // 统一追踪 Presence,确保消息路由正确
|
||
|
||
// 记录活跃对局到存储,用于全设备重连查找
|
||
nk.StorageWrite(ctx, []*runtime.StorageWrite{
|
||
{
|
||
Collection: "game_data",
|
||
Key: "active_match",
|
||
UserID: userID,
|
||
Value: fmt.Sprintf(`{"match_id":"%s"}`, matchID),
|
||
PermissionRead: 2, // Owner Read
|
||
PermissionWrite: 0, // Server Only
|
||
},
|
||
})
|
||
|
||
// 观众
|
||
if ms.Spectators[userID] {
|
||
publicState := core.CreatePublicGameState(ms.State)
|
||
data, _ := json.Marshal(publicState)
|
||
dispatcher.BroadcastMessage(core.OpCodeUpdateState, data, []runtime.Presence{presence}, nil, true)
|
||
continue
|
||
}
|
||
|
||
// 重连
|
||
if p, ok := ms.DisconnectedPlayers[userID]; ok {
|
||
p.SessionID = presence.GetSessionId()
|
||
ms.State.Players[userID] = p
|
||
delete(ms.DisconnectedPlayers, userID)
|
||
|
||
sanitized := ms.State.SanitizeForUser(userID)
|
||
data, err := json.Marshal(sanitized)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
dispatcher.BroadcastMessage(core.OpCodeUpdateState, data, []runtime.Presence{presence}, nil, true)
|
||
continue
|
||
}
|
||
|
||
// 新玩家
|
||
if _, exists := ms.State.Players[userID]; exists {
|
||
continue
|
||
}
|
||
|
||
tokenInfo := ms.ValidatedPlayers[userID]
|
||
if tokenInfo == nil {
|
||
continue
|
||
}
|
||
|
||
// 分配角色
|
||
char := "dog" // 回退
|
||
if ms.CharacterIndex < len(ms.CharacterPool) {
|
||
char = ms.CharacterPool[ms.CharacterIndex]
|
||
ms.CharacterIndex++
|
||
}
|
||
|
||
maxHP := ms.CharManager.GetInitialHP(char, ms.HPInit)
|
||
avatar := ms.CharManager.GetAvatar(char)
|
||
username := tokenInfo.Username
|
||
if username == "" {
|
||
username = presence.GetUsername()
|
||
}
|
||
|
||
player := &core.Player{
|
||
UserID: userID,
|
||
RealUserID: tokenInfo.UserID,
|
||
SessionID: presence.GetSessionId(),
|
||
Username: username,
|
||
Avatar: avatar,
|
||
HP: maxHP,
|
||
MaxHP: maxHP,
|
||
Character: char,
|
||
Ticket: tokenInfo.Ticket,
|
||
GameType: tokenInfo.GameType, // 从 token 中获取游戏类型(免费/付费)
|
||
Status: []string{},
|
||
RevealedCells: make(map[int]string),
|
||
}
|
||
|
||
ms.State.Players[userID] = player
|
||
ms.State.TurnOrder = append(ms.State.TurnOrder, userID)
|
||
ms.Presences[userID] = presence // 追踪 presence
|
||
}
|
||
|
||
// 更新 Label
|
||
ms.updateLabel(dispatcher)
|
||
|
||
// 开始游戏检查
|
||
if len(ms.State.Players) >= ms.MatchPlayerCount && !ms.State.GameStarted {
|
||
ms.State.GameStarted = true
|
||
ms.State.LastMoveTimestamp = time.Now().Unix()
|
||
|
||
// 打乱回合顺序
|
||
rand.Shuffle(len(ms.State.TurnOrder), func(i, j int) {
|
||
ms.State.TurnOrder[i], ms.State.TurnOrder[j] = ms.State.TurnOrder[j], ms.State.TurnOrder[i]
|
||
})
|
||
|
||
// 记录并消耗入场券
|
||
// ... 入场券消耗逻辑(由于是处理器保持简单)...
|
||
for _, p := range ms.State.Players {
|
||
go consumeTicket(p.RealUserID, p.Ticket, p.GameType, logger)
|
||
}
|
||
|
||
// 广播开始
|
||
broadcastUpdate(dispatcher, ms.State, core.OpCodeGameStart)
|
||
ms.updateLabel(dispatcher)
|
||
} else {
|
||
// 更新大厅
|
||
broadcastUpdate(dispatcher, ms.State, core.OpCodeUpdateState)
|
||
}
|
||
|
||
return ms
|
||
}
|
||
|
||
func (m *MatchHandler) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} {
|
||
ms := state.(*MatchState)
|
||
|
||
// 兜底销毁:房间存活超过 10 分钟强制销毁,防止异常房间泄漏
|
||
const maxRoomLifetimeSecs = 10 * 60
|
||
if time.Now().Unix()-ms.CreatedAt > maxRoomLifetimeSecs {
|
||
logger.Warn("MatchLoop: Room exceeded max lifetime (%ds), force destroying. CreatedAt=%d", maxRoomLifetimeSecs, ms.CreatedAt)
|
||
return nil
|
||
}
|
||
|
||
// 为此循环 tick 创建引擎实例
|
||
loopMatchID, _ := ctx.Value(runtime.RUNTIME_CTX_MATCH_ID).(string)
|
||
engine := logic.NewGameEngine(logger, dispatcher, ms.CharManager, ms.ItemManager, ms.Presences, ms.DisconnectedPlayers, loopMatchID)
|
||
|
||
for _, message := range messages {
|
||
userID := message.GetUserId()
|
||
opCode := message.GetOpCode()
|
||
|
||
logger.Debug("MatchLoop: Received msg from UserID=%s, OpCode=%d, DataLen=%d", userID, opCode, len(message.GetData()))
|
||
|
||
if ms.Spectators[userID] {
|
||
continue
|
||
}
|
||
|
||
switch opCode {
|
||
case core.OpCodeMove:
|
||
if !ms.State.GameStarted {
|
||
logger.Warn("Move ignored: game not started")
|
||
continue
|
||
}
|
||
var move core.MoveMessage
|
||
if err := json.Unmarshal(message.GetData(), &move); err != nil {
|
||
logger.Error("Failed to unmarshal move: %v", err)
|
||
continue
|
||
}
|
||
|
||
// 委托给引擎
|
||
engine.HandleMove(ms.State, userID, move.Index)
|
||
|
||
case core.OpCodeGetState:
|
||
sanitized := ms.State.SanitizeForUser(message.GetUserId())
|
||
data, _ := json.Marshal(sanitized)
|
||
targetPresence := ms.Presences[message.GetUserId()]
|
||
if targetPresence == nil {
|
||
// Presence 可能还未更新(重连时序问题),记录警告
|
||
logger.Warn("OpCodeGetState: Presence not found for user %s, possible timing issue", message.GetUserId())
|
||
continue
|
||
}
|
||
dispatcher.BroadcastMessage(core.OpCodeUpdateState, data, []runtime.Presence{targetPresence}, nil, true)
|
||
}
|
||
}
|
||
|
||
// 超时检查
|
||
if ms.State.GameStarted && ms.State.WinnerID == "" {
|
||
now := time.Now().Unix()
|
||
// Debug logging for timeout logic
|
||
if now-ms.State.LastMoveTimestamp >= int64(ms.State.TurnDuration) {
|
||
logger.Info("MatchLoop: Turn Timeout Triggered. Now: %d, Last: %d, Diff: %d, Duration: %d",
|
||
now, ms.State.LastMoveTimestamp, now-ms.State.LastMoveTimestamp, ms.State.TurnDuration)
|
||
|
||
currentUID := ms.State.TurnOrder[ms.State.CurrentTurnIndex]
|
||
|
||
if _, disconnected := ms.DisconnectedPlayers[currentUID]; disconnected {
|
||
// 跳过回合
|
||
engine.BroadcastEvent(core.GameEvent{Type: "status", Message: "⏰ 玩家断线,跳过回合"})
|
||
engine.AdvanceTurn(ms.State)
|
||
} else {
|
||
// 扣除HP并跳过
|
||
player := ms.State.Players[currentUID]
|
||
if player != nil {
|
||
engine.ApplyDamage(ms.State, player, 1, false) // 1点HP惩罚,非道具效果
|
||
engine.AdvanceTurn(ms.State)
|
||
}
|
||
}
|
||
ms.State.LastMoveTimestamp = now
|
||
|
||
// 检查游戏结束
|
||
if engine.CheckGameOver(ms.State) {
|
||
return ms
|
||
}
|
||
|
||
// 广播更新
|
||
broadcastUpdate(dispatcher, ms.State, core.OpCodeUpdateState)
|
||
}
|
||
}
|
||
|
||
// 检查游戏是否应该结束并销毁房间
|
||
if !ms.State.GameStarted && ms.State.WinnerID != "" {
|
||
logger.Info("Match %s is over (Winner: %s), destroying room", ms.State.WinnerID, ms.State.WinnerID)
|
||
// 游戏结束,清理所有参与者的对局存储
|
||
for uid := range ms.State.Players {
|
||
nk.StorageDelete(ctx, []*runtime.StorageDelete{
|
||
{
|
||
Collection: "game_data",
|
||
Key: "active_match",
|
||
UserID: uid,
|
||
},
|
||
})
|
||
}
|
||
for uid := range ms.DisconnectedPlayers {
|
||
nk.StorageDelete(ctx, []*runtime.StorageDelete{
|
||
{
|
||
Collection: "game_data",
|
||
Key: "active_match",
|
||
UserID: uid,
|
||
},
|
||
})
|
||
}
|
||
return nil
|
||
}
|
||
|
||
return ms
|
||
}
|
||
|
||
func (m *MatchHandler) MatchLeave(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} {
|
||
ms := state.(*MatchState)
|
||
|
||
for _, presence := range presences {
|
||
userID := presence.GetUserId()
|
||
if ms.Spectators[userID] {
|
||
delete(ms.Spectators, userID)
|
||
delete(ms.Presences, userID)
|
||
continue
|
||
}
|
||
|
||
if ms.State.GameStarted { // 将玩家标记为断开连接
|
||
if p, ok := ms.State.Players[userID]; ok {
|
||
ms.DisconnectedPlayers[userID] = p
|
||
p.DisconnectTime = time.Now().Unix()
|
||
delete(ms.State.Players, userID) // 从活跃玩家中移除
|
||
broadcastUpdate(dispatcher, ms.State, core.OpCodeUpdateState)
|
||
}
|
||
} else {
|
||
delete(ms.State.Players, userID)
|
||
// 退出大厅时,清理对局存储
|
||
nk.StorageDelete(ctx, []*runtime.StorageDelete{
|
||
{
|
||
Collection: "game_data",
|
||
Key: "active_match",
|
||
UserID: userID,
|
||
},
|
||
})
|
||
// 从回合顺序中移除...(如果我们支持离开大厅则需要逻辑)
|
||
// 找到并移除 userID
|
||
for i, id := range ms.State.TurnOrder {
|
||
if id == userID {
|
||
ms.State.TurnOrder = append(ms.State.TurnOrder[:i], ms.State.TurnOrder[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
delete(ms.Presences, userID) // 移除 presence
|
||
}
|
||
|
||
ms.updateLabel(dispatcher)
|
||
return ms
|
||
}
|
||
|
||
func (ms *MatchState) updateLabel(dispatcher runtime.MatchDispatcher) {
|
||
label := MatchLabel{
|
||
Open: !ms.State.GameStarted && len(ms.State.Players) < ms.MatchPlayerCount,
|
||
Started: ms.State.GameStarted,
|
||
PlayerCount: len(ms.State.Players),
|
||
MaxPlayers: ms.MatchPlayerCount,
|
||
Label: "Animal Minesweeper",
|
||
GameType: ms.GameType, // 包含游戏类型
|
||
}
|
||
labelBytes, _ := json.Marshal(label)
|
||
_ = dispatcher.MatchLabelUpdate(string(labelBytes))
|
||
}
|
||
|
||
func (m *MatchHandler) MatchTerminate(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} {
|
||
return state
|
||
}
|
||
|
||
func (m *MatchHandler) MatchSignal(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, data string) (interface{}, string) {
|
||
return state, data
|
||
}
|
||
|
||
// 辅助函数
|
||
|
||
func broadcastUpdate(dispatcher runtime.MatchDispatcher, state *core.GameState, opCode int64) {
|
||
// 原始逻辑:向每个玩家发送已脱敏的状态
|
||
// 我们需要遍历已连接的 presence 吗?
|
||
// 或者只是广播已脱敏的通用状态?
|
||
// 原始 main.go 循环:
|
||
// for _, presence := range presences ... SanitizeForUser ...
|
||
// 由于我们在全局函数中无法轻松获取 presence,我们要依赖玩家映射的键吗?
|
||
// 但是 Dispatcher 需要 presence 作为目标。
|
||
// BroadcastMessage 使用 nil presence 会发送给所有人。
|
||
// 但是我们需要针对每个用户的脱敏来处理隐藏格子。
|
||
// Sanitize() 方法(无参数)清除所有已揭示的格子。这对公开广播是安全的。
|
||
// 并且它更新所有人。
|
||
// 但是正确的扫雷游戏需要用户看到他们自己揭示的格子。
|
||
// 所以我们现在应该广播安全的通用版本,除非我们追踪 presence。
|
||
|
||
// 简化:广播通用的已脱敏状态(如果不是公开的,每个人都看到揭示的格子是隐藏的?)
|
||
// 等等,Sanitize() 清除了 Player 中的 RevealedCells 映射。
|
||
// SanitizeForUser(uid) 保留该用户的格子。
|
||
|
||
// 如果我们使用 dispatcher.BroadcastMessage(..., nil, ...) 它会发送给所有人。
|
||
// 没有 Presence 列表,我们无法在这里实现每用户的视图。
|
||
// MatchState 通常不存储 presence 列表,Nakama 会管理它。
|
||
// 如果我们需要完美的实现,可能需要重新考虑这一点。
|
||
// 然而,原始 main.go 的 broadcastStateToAllPlayers 需要传递 presence。
|
||
// 在 MatchLoop 中,我们没有在消息中传递所有已连接用户的 presence 列表。
|
||
|
||
// 原始代码 MatchJoin 使用 dispatcher.BroadcastMessage(..., nil) 广播,这会向所有人发送一条消息。
|
||
// 而且 Sanitize() 清除了所有私有信息。
|
||
// 逻辑 handleMove 也广播了 Sanitize()。
|
||
// 所以原始代码实际上并没有发送私有信息?
|
||
// 让我们重读 main.go。
|
||
|
||
// 第 319 行:Sanitize() 清除所有 revealedCells。
|
||
// 第 413 行:broadcastStateToAllPlayers 循环 presence...
|
||
// 但是 MatchLoop 调用 handleMove(第 1158 行)调用了 dispatcher.BroadcastMessage(..., state.Sanitize()...)!
|
||
// 所以在原始代码中,RevealedCells 从未通过广播更新显示给客户端?
|
||
// 放大镜事件发送 CellIndex 和 CellType。客户端使用该事件在本地更新地图?
|
||
// 是的:broadcastEvent 发送 CellIndex 和 CellType。
|
||
// 所以如果事件携带私有信息,状态更新本身不需要携带它。
|
||
|
||
// 所以 Sanitize() 就足够了。
|
||
|
||
data, _ := json.Marshal(state.Sanitize())
|
||
dispatcher.BroadcastMessage(opCode, data, nil, nil, true)
|
||
}
|
||
|
||
func consumeTicket(realUserID int64, ticket string, gameType string, logger runtime.Logger) {
|
||
if realUserID <= 0 {
|
||
return
|
||
}
|
||
|
||
reqBody, _ := json.Marshal(map[string]string{
|
||
"user_id": fmt.Sprintf("%d", realUserID),
|
||
"game_code": gameType, // 使用实际的游戏类型,免费模式后端会跳过扣减
|
||
"ticket": ticket,
|
||
})
|
||
|
||
url := config.BackendBaseURL + "/game/consume-ticket"
|
||
|
||
resp, err := config.MakeInternalRequest("POST", url, reqBody)
|
||
if err != nil {
|
||
logger.Error("Failed to call backend consume-ticket API for user %d: %v", realUserID, err)
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
logger.Error("Backend consume-ticket returned status %d for user %d", resp.StatusCode, realUserID)
|
||
} else {
|
||
logger.Info("Ticket consumed successfully for user %d", realUserID)
|
||
}
|
||
}
|