diff --git a/common/cool/cool.go b/common/cool/cool.go index 7614dee7f..c5fa7c2d0 100644 --- a/common/cool/cool.go +++ b/common/cool/cool.go @@ -95,3 +95,17 @@ func Fail(message string) *BaseRes { // } // return nil, nil // } + +func RedisDo(ctx context.Context, funcstring string, a ...any) { + + conn, err := g.Redis("cool").Conn(ctx) + if err != nil { + panic(err) + } + + defer conn.Close(ctx) + _, err = conn.Do(ctx, "publish", funcstring, a) + if err != nil { + panic(err) + } +} diff --git a/common/cool/func.go b/common/cool/func.go index 681f7b629..2e69285da 100644 --- a/common/cool/func.go +++ b/common/cool/func.go @@ -136,6 +136,7 @@ func ListenFunc(ctx g.Ctx) { continue } Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic) + _, err = conn.Do(ctx, "subscribe", "sun:join:2458") //加入队列 // 4. 循环接收消息 connError := false diff --git a/common/socket/ServerEvent.go b/common/socket/ServerEvent.go index cbfb86692..bee984e1f 100644 --- a/common/socket/ServerEvent.go +++ b/common/socket/ServerEvent.go @@ -88,6 +88,7 @@ func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) { //v.LF.Close() // v.LF.Close() + close(v.MsgChan) if v.Player != nil { v.Player.Save() //保存玩家数据 diff --git a/logic/controller/fight_巅峰.go b/logic/controller/fight_巅峰.go index b0b429f89..9edee7639 100644 --- a/logic/controller/fight_巅峰.go +++ b/logic/controller/fight_巅峰.go @@ -1 +1,30 @@ package controller + +import ( + "blazing/common/socket/errorcode" + "blazing/cool" + "blazing/logic/service/common" + "blazing/logic/service/fight" + "blazing/logic/service/fight/top/repo" + "blazing/logic/service/player" + "context" + + goredis "github.com/redis/go-redis/v9" +) + +// 表示"宠物王加入"的入站消息数据 +type PetTOPLEVELnboundInfo struct { + Head common.TomeeHeader `cmd:"2458" struc:"skip"` + Mode uint32 //巅峰赛对战模式 19 = 普通模式单精灵 20 = 普通模式多精灵 + +} + +func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { + cool.RedisDo(context.TODO(), "sun:join:2458", data.Head.UserID, data.Mode) + client := cool.Redis.Client() + + // 类型断言为 UniversalClient + universalClient, _ := client.(goredis.UniversalClient) + repo.NewPlayerRepository(universalClient).AddPlayerToPool(context.TODO(), data.Head.UserID, 1) + return nil, -1 +} diff --git a/logic/service/fight/top/repo/lua.go b/logic/service/fight/top/repo/lua.go new file mode 100644 index 000000000..363030ef2 --- /dev/null +++ b/logic/service/fight/top/repo/lua.go @@ -0,0 +1,60 @@ +package repo + +import "github.com/redis/go-redis/v9" + +// AtomicMatchScript 用于在 ZSet 中原子化查询、提取并删除玩家 +// KEYS[1]: ZSet 的 key +// ARGV[1]: 当前玩家的分数 +// ARGV[2]: 分数差范围 (delta) +// ARGV[3]: 最大返回数量 +// 返回: 匹配的玩家ID列表 (已从ZSet中删除) +var AtomicMatchScript = redis.NewScript(` +local key = KEYS[1] +local score = tonumber(ARGV[1]) +local delta = tonumber(ARGV[2]) +local limit = tonumber(ARGV[3]) + +local min_score = score - delta +local max_score = score + delta + +-- 查询分数范围内的玩家 +local members = redis.call('ZRANGEBYSCORE', key, min_score, max_score, 'LIMIT', 0, limit) + +if #members == 0 then + return {} +end + +-- 原子删除匹配的玩家 +for i, member in ipairs(members) do + redis.call('ZREM', key, member) +end + +return members +`) + +// CompositeScoreScript 用于将分数和时间戳合并为复合分数 +// KEYS[1]: ZSet 的 key +// ARGV[1]: 玩家ID +// ARGV[2]: 玩家分数 (整数部分) +// ARGV[3]: 时间戳 (用于小数部分,确保先入队的玩家优先匹配) +// 复合分数格式: score.timestamp (分数相同时,时间戳小的优先) +var CompositeScoreScript = redis.NewScript(` +local key = KEYS[1] +local player_id = ARGV[1] +local score = tonumber(ARGV[2]) +local timestamp = tonumber(ARGV[3]) + +-- 将时间戳转换为小数部分 (归一化到0-1之间) +-- 使用较大的除数确保时间戳差异体现在小数部分 +-- 时间戳越小,复合分数越小,优先级越高 +local max_timestamp = 10000000000000 -- 足够大的值来归一化时间戳 +local decimal_part = timestamp / max_timestamp + +-- 复合分数 = 整数分数 + 时间戳小数部分 +local composite_score = score + decimal_part + +-- 添加玩家到 ZSet +redis.call('ZADD', key, composite_score, player_id) + +return composite_score +`) diff --git a/logic/service/fight/top/repo/player.go b/logic/service/fight/top/repo/player.go new file mode 100644 index 000000000..bb0208ce3 --- /dev/null +++ b/logic/service/fight/top/repo/player.go @@ -0,0 +1,105 @@ +package repo + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" +) + +const ( + // MatchPoolKey 匹配池的 Redis Key + MatchPoolKey = "{match:pool}" + // GlobalRankKey 全局排行榜的 Redis Key + GlobalRankKey = "{rank:global}" +) + +// PlayerRepository 玩家数据仓库 +type PlayerRepository struct { + client redis.UniversalClient +} + +// NewPlayerRepository 创建 PlayerRepository 实例 +func NewPlayerRepository(client redis.UniversalClient) *PlayerRepository { + return &PlayerRepository{ + client: client, + } +} + +// AddPlayerToPool 将玩家添加到匹配池 +// 使用 CompositeScoreScript 计算复合分数,确保先入队的玩家优先匹配 +func (r *PlayerRepository) AddPlayerToPool(ctx context.Context, playerID uint32, score int64) error { + timestamp := time.Now().UnixMilli() + + _, err := CompositeScoreScript.Run(ctx, r.client, + []string{MatchPoolKey}, + playerID, + score, + timestamp, + ).Result() + + return err +} + +// SearchAndPickPlayers 原子化地从匹配池中查询、提取并删除玩家 +// 返回匹配到的玩家ID列表 +func (r *PlayerRepository) SearchAndPickPlayers(ctx context.Context, minScore, maxScore int64, count int) ([]string, error) { + // 计算中心分数和差值范围 + centerScore := (minScore + maxScore) / 2 + delta := (maxScore - minScore) / 2 + + result, err := AtomicMatchScript.Run(ctx, r.client, + []string{MatchPoolKey}, + centerScore, + delta, + count, + ).Result() + + if err != nil { + return nil, err + } + + // 将结果转换为字符串切片 + members, ok := result.([]interface{}) + if !ok { + return []string{}, nil + } + + players := make([]string, 0, len(members)) + for _, m := range members { + if playerID, ok := m.(string); ok { + players = append(players, playerID) + } + } + + return players, nil +} + +// GetGlobalRank 获取全局排行榜前 N 名 +// 返回玩家ID和分数的列表,按分数从高到低排序 +func (r *PlayerRepository) GetGlobalRank(ctx context.Context, topN int64) ([]redis.Z, error) { + return r.client.ZRevRangeWithScores(ctx, GlobalRankKey, 0, topN-1).Result() +} + +// UpdatePlayerScore 更新玩家在全局排行榜中的分数 +func (r *PlayerRepository) UpdatePlayerScore(ctx context.Context, playerID string, score float64) error { + return r.client.ZAdd(ctx, GlobalRankKey, redis.Z{ + Score: score, + Member: playerID, + }).Err() +} + +// RemovePlayerFromPool 从匹配池中移除玩家 +func (r *PlayerRepository) RemovePlayerFromPool(ctx context.Context, playerID string) error { + return r.client.ZRem(ctx, MatchPoolKey, playerID).Err() +} + +// GetPlayerRank 获取玩家在全局排行榜中的排名 (0-based, 分数从高到低) +func (r *PlayerRepository) GetPlayerRank(ctx context.Context, playerID string) (int64, error) { + return r.client.ZRevRank(ctx, GlobalRankKey, playerID).Result() +} + +// GetPlayerScore 获取玩家在全局排行榜中的分数 +func (r *PlayerRepository) GetPlayerScore(ctx context.Context, playerID string) (float64, error) { + return r.client.ZScore(ctx, GlobalRankKey, playerID).Result() +}