feat(space): 替换并发安全map实现以提升性能

将原来基于`utils.ConcurrentMap`的玩家存储结构替换为
`github.com/mhmtszr/concurrent-swiss-map`提供的`CsMap`,
以获得更高效的并发读写能力。

同时修改了相关API调用方式:
- `Set` 改为 `Store`
- `Remove` 改为 `Delete`
- `IterCb` 改为 `Range`,并支持提前终止迭代
- `Items()` 不再使用

此外,调整了部分业务逻辑中对玩家列表遍历的方式,
确保在发送网络包后及时跳出循环,避免不必要的操作。

新增战斗类型处理函数`PET_King`用于处理宠物王相关的
战斗请求,并修复了`PET_MELEE`方法中的逻辑问题。

更新了go.mod和go.sum引入新的依赖库。
```
This commit is contained in:
2025-11-15 15:22:58 +08:00
parent f919047ff6
commit 6979b7018d
19 changed files with 117 additions and 48 deletions

View File

@@ -281,7 +281,7 @@ func (m ConcurrentMap[K, V]) Items() map[K]V {
// maps. RLock is held for all calls for a given shard
// therefore callback sess consistent view of a shard,
// but not across the shards
type IterCb[K comparable, V any] func(key K, v V)
type IterCb[K comparable, V any] func(key K, v V) bool
// Callback based iterator, cheapest way to read
// all elements in a map.
@@ -290,7 +290,10 @@ func (m ConcurrentMap[K, V]) IterCb(fn IterCb[K, V]) {
shard := (m.shards)[idx]
shard.RLock()
for key, value := range shard.items {
fn(key, value)
if !fn(key, value) {
break
}
}
shard.RUnlock()
}

View File

@@ -30,8 +30,9 @@ func (h *Controller) ChangePlayerName(data *user.ChangePlayerNameInboundInfo, c
UserID: c.Info.UserID,
}
space.GetSpace(c.Info.MapID).User.IterCb(func(playerID uint32, player common.PlayerI) {
space.GetSpace(c.Info.MapID).User.Range(func(playerID uint32, player common.PlayerI) bool {
player.SendPack(data.Head.Pack(&result))
return false
})
return result, 0

View File

@@ -17,9 +17,37 @@ func (h Controller) PET_MELEE(data *fight.StartPetWarInboundInfo, c *player.Play
c.PVPinfo = &info.PVPinfo{
Mode: info.BattleMode.PET_MELEE,
Status: info.BattleStatus.FIGHT_WITH_PLAYER}
g := c.PET_MELEE()
g := c.Pet_joinFight()
if g != nil {
fight.NewFight(info.BattleMode.PET_MELEE, info.BattleStatus.FIGHT_WITH_PLAYER, c.PET_MELEE(), c) ///开始对战,房主方以及被邀请方
fight.NewFight(info.BattleMode.PET_MELEE, info.BattleStatus.FIGHT_WITH_PLAYER, g, c) ///开始对战,房主方以及被邀请方
}
return
}
func (h Controller) PET_King(data *fight.PetKingJoinInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if !c.CanFight() {
return nil, errorcode.ErrorCodes.ErrPokemonNotEligible
}
c.PVPinfo = &info.PVPinfo{
Status: info.BattleStatus.FIGHT_WITH_PLAYER}
switch data.Type {
case 5:
c.PVPinfo.Mode = info.BattleMode.SINGLE_MODE
case 6:
c.PVPinfo.Mode = info.BattleMode.MULTI_MODE
}
g := c.Pet_joinFight()
if g != nil {
switch data.Type {
case 5:
fight.NewFight(info.BattleMode.SINGLE_MODE, info.BattleStatus.FIGHT_WITH_PLAYER, g, c) ///开始对战,房主方以及被邀请方
case 6:
fight.NewFight(info.BattleMode.MULTI_MODE, info.BattleStatus.FIGHT_WITH_PLAYER, g, c) ///开始对战,房主方以及被邀请方
}
}
return

View File

@@ -76,10 +76,11 @@ func (h Controller) ChangePlayerCloth(data *item.ChangePlayerClothInboundInfo, c
result.ClothList = append(result.ClothList, model.PeopleItemInfo{ID: v, Level: 1})
}
c.Info.Clothes = result.ClothList
space.GetSpace(c.Info.MapID).User.IterCb(func(playerID uint32, player common.PlayerI) {
space.GetSpace(c.Info.MapID).User.Range(func(playerID uint32, player common.PlayerI) bool {
// fmt.Println("ChangePlayerCloth", playerID, data.Head.Pack(result))
data.Head.Result = 0
player.SendPack(data.Head.Pack(result))
return false
})
return nil, -1

View File

@@ -106,11 +106,11 @@ func (h *Controller) Login(data *user.MAIN_LOGIN_IN, c gnet.Conn) (result *user.
//copier.Copy(t.Info, tt)
t1 := player.NewTomeeHeader(2001, t.Info.UserID)
space.GetSpace(t.Info.MapID).User.IterCb(func(playerID uint32, player common.PlayerI) {
space.GetSpace(t.Info.MapID).User.Range(func(playerID uint32, player common.PlayerI) bool {
player.SendPack(t1.Pack(tt))
return false
})
space.GetSpace(t.Info.MapID).User.Set(t.Info.UserID, t)
space.GetSpace(t.Info.MapID).User.Store(t.Info.UserID, t)
}()
return result, 0

View File

@@ -15,7 +15,7 @@ import (
func (h *Controller) MapEnter(data *maps.InInfo, c *player.Player) (result *maps.OutInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
c.Info.MapID = data.MapId //登录地图
space.GetSpace(c.Info.MapID).User.Set(c.Info.UserID, c) //添加玩家
space.GetSpace(c.Info.MapID).User.Store(c.Info.UserID, c) //添加玩家
result = maps.NewOutInfo()
c.Info.Pos = data.Point
@@ -39,7 +39,7 @@ func (h *Controller) MapLeave(data *maps.LeaveMapInboundInfo, c *player.Player)
c.Canmon = false
c.Changemap = true //可以刷怪
data.Broadcast(c.Info.MapID, space.LeaveMapOutboundInfo{UserID: c.Info.UserID}) //同步广播
space.GetSpace(c.Info.MapID).User.Remove(c.Info.UserID)
space.GetSpace(c.Info.MapID).User.Delete(c.Info.UserID)
// 如果有正在运行的刷怪协程,发送停止信号
c.Info.MapID = 0 // 重置当前地图
@@ -49,11 +49,12 @@ func (h *Controller) MapList(data *maps.ListMapPlayerInboundInfo, c *player.Play
result = &maps.ListMapPlayerOutboundInfo{}
result.Player = make([]maps.OutInfo, 0)
space.GetSpace(c.Info.MapID).User.IterCb(func(playerID uint32, player common.PlayerI) {
space.GetSpace(c.Info.MapID).User.Range(func(playerID uint32, player common.PlayerI) bool {
result1 := maps.NewOutInfo()
copier.CopyWithOption(result1, player.GetInfo(), copier.Option{DeepCopy: true})
result.Player = append(result.Player, *result1)
result.Player = LastFourElements(result.Player)
return false
})
c.Canmon = true //可以刷怪

View File

@@ -37,7 +37,7 @@ func (h Controller) UserMoreInfo(data *user.MoreUserInfoInboundInfo, c *player.P
func (h Controller) Aimat(data *user.AimatInboundInfo, c *player.Player) (result *user.AimatOutboundInfo, err errorcode.ErrorCode) {
defer space.GetSpace(c.Info.MapID).User.IterCb(func(playerID uint32, player common.PlayerI) {
defer space.GetSpace(c.Info.MapID).User.Range(func(playerID uint32, player common.PlayerI) bool {
ret := &user.AimatOutboundInfo{
ItemId: data.ItemId,
@@ -47,14 +47,14 @@ func (h Controller) Aimat(data *user.AimatInboundInfo, c *player.Player) (result
}
data.Head.Result = 0
player.SendPack(data.Head.Pack(ret))
return false
})
return nil, -1
}
func (h Controller) Chat(data *user.ChatInboundInfo, c *player.Player) (result *user.ChatOutboundInfo, err errorcode.ErrorCode) {
defer space.GetSpace(c.Info.MapID).User.IterCb(func(playerID uint32, v common.PlayerI) {
defer space.GetSpace(c.Info.MapID).User.Range(func(playerID uint32, v common.PlayerI) bool {
result = &user.ChatOutboundInfo{
Message: string([]byte(data.Message)[:data.MessageLen-1]),
@@ -64,6 +64,7 @@ func (h Controller) Chat(data *user.ChatInboundInfo, c *player.Player) (result *
result.Message = cool.Filter.Replace(result.Message, '*')
data.Head.Result = 0
v.SendPack(data.Head.Pack(result))
return false
})
return nil, -1
@@ -76,7 +77,7 @@ func (h Controller) ChangePlayerColor(data *user.ChangeColorInboundInfo, c *play
c.Info.Color = data.Color
c.Info.Texture = 0
defer space.GetSpace(c.Info.MapID).User.IterCb(func(playerID uint32, v common.PlayerI) {
defer space.GetSpace(c.Info.MapID).User.Range(func(playerID uint32, v common.PlayerI) bool {
data.Head.Result = 0
result = &user.ChangeColorOutboundInfo{
UserId: c.Info.UserID,
@@ -85,6 +86,7 @@ func (h Controller) ChangePlayerColor(data *user.ChangeColorInboundInfo, c *play
Texture: c.Info.Texture,
}
v.SendPack(data.Head.Pack(result))
return false
})
return nil, -1
@@ -95,7 +97,7 @@ func (h Controller) ChangePlayerDoodle(data *user.ChangeDoodleInboundInfo, c *pl
}
c.Info.Texture = data.Id
c.Info.Color = data.Color
defer space.GetSpace(c.Info.MapID).User.IterCb(func(playerID uint32, v common.PlayerI) {
defer space.GetSpace(c.Info.MapID).User.Range(func(playerID uint32, v common.PlayerI) bool {
data.Head.Result = 0
result = &user.ChangeDoodleOutboundInfo{
UserId: c.Info.UserID,
@@ -104,6 +106,7 @@ func (h Controller) ChangePlayerDoodle(data *user.ChangeDoodleInboundInfo, c *pl
Texture: c.Info.Texture,
}
v.SendPack(data.Head.Pack(result))
return false
})
return nil, -1

View File

@@ -39,6 +39,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mhmtszr/concurrent-swiss-map v1.0.9 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/redis/go-redis/v9 v9.5.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect

View File

@@ -22,6 +22,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -69,6 +70,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mhmtszr/concurrent-swiss-map v1.0.9 h1:ijAlVG/QHC4A4FRdfdtq0oRJ03Zx9dsF8RSiBQB/gKk=
github.com/mhmtszr/concurrent-swiss-map v1.0.9/go.mod h1:F6QETL48Qn7jEJ3ZPt7EqRZjAAZu7lRQeQGIzXuUIDc=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
@@ -77,6 +80,7 @@ github.com/panjf2000/gnet/v2 v2.9.1 h1:bKewICy/0xnQ9PMzNaswpe/Ah14w1TrRk91LHTcbI
github.com/panjf2000/gnet/v2 v2.9.1/go.mod h1:WQTxDWYuQ/hz3eccH0FN32IVuvZ19HewEWx0l62fx7E=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pointernil/bitset32 v0.0.1 h1:GL7o6ehG4W5ztXic75ncAmKzSxYQRSKxyaz2wOdz2aM=
github.com/pointernil/bitset32 v0.0.1/go.mod h1:naghmAYTkvJhKSCVPbBwLAXkHMF2WBIJ1bJpnFpmQwI=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=

View File

@@ -35,6 +35,13 @@ type StartPetWarInboundInfo struct {
Head player.TomeeHeader `cmd:"2431" struc:"[0]pad"`
}
// 表示"宠物王加入"的入站消息数据
type PetKingJoinInboundInfo struct {
Head player.TomeeHeader `cmd:"2413" struc:"[0]pad"`
Type uint32 // 战斗类型5=单精灵6=多精灵11=精灵大师赛 (对应Java的@UInt long type)
FightType uint32 // 仅当Type为11时有效 (对应Java的@UInt long fightType)
}
// HandleFightInviteInboundInfo 处理战斗邀请的入站消息
type HandleFightInviteInboundInfo struct {

View File

@@ -67,5 +67,5 @@ func (e *EffectAttackMiss) Skill_Hit_ex() bool {
// 设置参数:复用父类逻辑,设置持续回合
func (e *EffectAttackMiss) SetArgs(t *input.Input, a ...int) {
e.EffectNode.SetArgs(t, a...)
e.EffectNode.Duration(e.EffectNode.SideEffectArgs[0]) // 持续回合由SideEffectArgs[0]指定
e.EffectNode.Duration(e.EffectNode.SideEffectArgs[0] - 1) // 持续回合由SideEffectArgs[0]指定
}

View File

@@ -88,10 +88,7 @@ func (e *EffectPhysicalAttackAddStatus) SetArgs(t *input.Input, a ...int) {
// 计算持续回合(封装为独立方法,增强可读性)
func (e *EffectPhysicalAttackAddStatus) getDuration() int {
// 优先使用配置的回合数SideEffectArgs[1]
if len(e.EffectNode.SideEffectArgs) > 1 {
return e.EffectNode.SideEffectArgs[1]
}
// 默认随机2~3回合Int31n(2)返回0/1+2后为2/3
return int(e.Input.FightC.GetRand().Int31n(2)) + 2
}

View File

@@ -1,6 +1,7 @@
package maps
import (
"blazing/logic/service/common"
"blazing/logic/service/player"
"blazing/logic/service/space"
@@ -24,11 +25,14 @@ type InInfo struct {
func (t *InInfo) Broadcast(mapid uint32, o OutInfo) {
for _, v := range space.GetSpace(mapid).User.Items() {
space.GetSpace(mapid).User.Range(func(k uint32, v common.PlayerI) (stop bool) {
t.Head.Result = 0
v.SendPack(t.Head.Pack(&o))
}
return false
})
}
// 这里存储星球的map

View File

@@ -11,10 +11,11 @@ type LeaveMapInboundInfo struct {
}
func (t *LeaveMapInboundInfo) Broadcast(mapid uint32, o space.LeaveMapOutboundInfo) {
space.GetSpace(mapid).User.IterCb(func(playerID uint32, player common.PlayerI) {
space.GetSpace(mapid).User.Range(func(playerID uint32, player common.PlayerI) bool {
t.Head.Result = 0
player.SendPack(t.Head.Pack(&o))
return false
})
}

View File

@@ -30,10 +30,11 @@ func (t *WalkInInfo) Broadcast(mapid uint32, o WalkOutInfo) {
if !limiter.Allow() {
return
}
space.GetSpace(mapid).User.IterCb(func(playerID uint32, player common.PlayerI) {
space.GetSpace(mapid).User.Range(func(playerID uint32, player common.PlayerI) bool {
t.Head.Result = 0
tt := t.Head.Pack(&o)
player.SendPack(tt)
return false
})
}

View File

@@ -1,6 +1,7 @@
package pet
import (
"blazing/logic/service/common"
"blazing/logic/service/player"
"blazing/logic/service/space"
"blazing/modules/blazing/model"
@@ -39,12 +40,13 @@ type PetShowInboundInfo struct {
}
func (t *PetShowInboundInfo) Broadcast(mapid uint32, o PetShowOutboundInfo) {
for _, v := range space.GetSpace(mapid).User.Items() {
space.GetSpace(mapid).User.Range(func(key uint32, v common.PlayerI) (stop bool) {
t.Head.Result = 0
v.SendPack(t.Head.Pack(&o))
}
return false
})
}
type PetShowOutboundInfo struct {

View File

@@ -11,7 +11,7 @@ import (
func (lw *Player) InvitePlayerToBattle() {
pinfo := lw.PVPinfo
space.GetSpace(lw.Info.MapID).User.IterCb(func(key uint32, v common.PlayerI) {
space.GetSpace(lw.Info.MapID).User.Range(func(key uint32, v common.PlayerI) bool {
value := v.(*Player)
if key == uint32(pinfo.PlayerID) { //说明这里是针对玩家邀请的
@@ -24,30 +24,31 @@ func (lw *Player) InvitePlayerToBattle() {
Mode: pinfo.Mode,
}
value.SendPack(t1.Pack(&t))
return
return true
}
return false
})
}
func (p *Player) PET_MELEE() *Player {
func (p *Player) Pet_joinFight() *Player {
var lw *Player
//pinfo := lw.PVPinfo
space.GetSpace(p.Info.MapID).User.IterCb(func(key uint32, v common.PlayerI) {
space.GetSpace(p.Info.MapID).User.Range(func(key uint32, v common.PlayerI) bool {
value := v.(*Player)
if value.PVPinfo != nil && value != p {
//确认是乱斗模式
if value.PVPinfo.Mode == info.BattleMode.PET_MELEE {
lw.PVPinfo = nil //先将自身的准备信息置空
if *value.PVPinfo == *p.PVPinfo {
p.PVPinfo = nil //先将自身的准备信息置空
value.PVPinfo = nil
lw = value
return
return true
}
}
return false
})
return lw
}

View File

@@ -311,11 +311,13 @@ func LeaveMap(c common.PlayerI) {
}
t := NewTomeeHeader(2002, c.GetInfo().UserID)
for k, v := range space.GetSpace(c.GetInfo().MapID).User.Items() {
space.GetSpace(c.GetInfo().MapID).User.Range(func(k uint32, v common.PlayerI) (stop bool) {
if k != c.GetInfo().UserID {
v.SendPack(t.Pack(&space.LeaveMapOutboundInfo{UserID: c.GetInfo().UserID}))
}
return false
})
}
space.GetSpace(c.GetInfo().MapID).User.Remove(c.GetInfo().UserID)
space.GetSpace(c.GetInfo().MapID).User.Delete(c.GetInfo().UserID)
}

View File

@@ -3,13 +3,14 @@ package space
import (
"blazing/common/data/xmlres"
"blazing/common/utils"
"blazing/logic/service/common"
csmap "github.com/mhmtszr/concurrent-swiss-map"
)
// Space 针对Player的并发安全map键为uint32类型
type Space struct {
User utils.ConcurrentMap[uint32, common.PlayerI] // 存储玩家数据的map键为玩家ID
User *csmap.CsMap[uint32, common.PlayerI] // 存储玩家数据的map键为玩家ID
CanRefresh bool //是否能够刷怪
//ID uint32 // 地图ID
Name string //地图名称
@@ -21,9 +22,20 @@ type Space struct {
// NewSyncMap 创建一个新的玩家同步map
func NewSpace() *Space {
return &Space{
User: utils.NewWithCustomShardingFunction[uint32, common.PlayerI](func(key uint32) uint32 {
return key
}),
User: csmap.New[uint32, common.PlayerI](
// set the number of map shards. the default value is 32.
csmap.WithShardCount[uint32, common.PlayerI](32),
// // if don't set custom hasher, use the built-in maphash.
// csmap.WithCustomHasher[string, int](func(key string) uint64 {
// hash := fnv.New64a()
// hash.Write([]byte(key))
// return hash.Sum64()
// }),
// set the total capacity, every shard map has total capacity/shard count capacity. the default value is 0.
// csmap.WithSize[string, int](1000),
),
}
}