diff --git a/common/cool/rpc.go b/common/cool/rpc.go index 7401bdf59..c4792f7c8 100644 --- a/common/cool/rpc.go +++ b/common/cool/rpc.go @@ -6,6 +6,16 @@ func AddClient(id uint32, client *ClientHandler) { Clientmap.Store(id, client) // sync.Map存值 } +// 清理指定client(uid=100000*onlineID+port) +func DeleteClientOnly(uid uint32) { + Clientmap.Delete(uid) +} + +// 清理指定client(onlineID+port) +func DeleteClient(id, port uint32) { + Clientmap.Delete(100000*id + port) +} + // 取值示例 func GetClient(id, port uint32) (*ClientHandler, bool) { // 普通map:client, ok := Clientmap[id] diff --git a/common/rpc/rpc.go b/common/rpc/rpc.go index 428820c19..981cad95c 100644 --- a/common/rpc/rpc.go +++ b/common/rpc/rpc.go @@ -1,106 +1,149 @@ -package rpc - -import ( - "blazing/common/data/share" - "blazing/cool" - - "context" - "fmt" - "log" - - config "blazing/modules/config/service" - - "github.com/filecoin-project/go-jsonrpc" - "github.com/gogf/gf/v2/util/gconv" -) - -// Define the server handler -type ServerHandler struct{} - -// 实现踢人 -func (*ServerHandler) Kick(_ context.Context, userid uint32) error { - - useid1, _ := share.ShareManager.GetUserOnline(userid) - if useid1 == 0 { - return nil - } - - cl, ok := cool.GetClientOnly(useid1) - if !ok { - return nil - } - cl.KickPerson(userid) //实现指定服务器踢人 - return nil -} - -// 注册logic服务器 -func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error { - fmt.Println("注册logic服务器", id, port) - - //TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug - revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx) - if !ok { - return fmt.Errorf("no reverse client") - } - t := config.NewServerService().GetServerID((id)) - - aa, ok := cool.GetClient(t.OnlineID, t.Port) - if ok && aa != nil { //如果已经存在且这个端口已经被存过 - aa.QuitSelf(0) - } - cool.AddClient(100000*id+port, &revClient) - - //Refurh() - return nil - -} - -func CServer() *jsonrpc.RPCServer { - // create a new server instance - rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler]("")) - - rpcServer.Register("", &ServerHandler{}) - - return rpcServer - -} - -var closer jsonrpc.ClientCloser - -func StartClient(id, port uint32, callback any) *struct { - Kick func(uint32) error - - RegisterLogic func(uint32, uint32) error -} { - //cool.Config.File.Domain = "127.0.0.1" - var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc" - - closer1, err := jsonrpc.NewMergeClient(context.Background(), - rpcaddr, "", []interface{}{ - &RPCClient, - }, nil, jsonrpc.WithClientHandler("", callback), - jsonrpc.WithReconnFun(func() { RPCClient.RegisterLogic(id, port) }), - ) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - //if port != 0 { //注册logic - defer RPCClient.RegisterLogic(id, port) - - //} - - closer = closer1 - - return &RPCClient -} - -// Setup RPCClient with reverse call handler -var RPCClient struct { - Kick func(uint32) error //踢人 - - RegisterLogic func(uint32, uint32) error - - // UserLogin func(int32, int32) error //用户登录事件 - // UserLogout func(int32, int32) error //用户登出事件 -} +package rpc + +import ( + "blazing/common/data/share" + "blazing/cool" + + "context" + "fmt" + "log" + "time" + + config "blazing/modules/config/service" + + "github.com/filecoin-project/go-jsonrpc" + "github.com/gogf/gf/v2/util/gconv" +) + +// Define the server handler +type ServerHandler struct{} + +const kickForwardTimeout = 3 * time.Second + +// 实现踢人 +func (*ServerHandler) Kick(_ context.Context, userid uint32) error { + useid1, err := share.ShareManager.GetUserOnline(userid) + if err != nil || useid1 == 0 { + // 请求到达时用户已离线,直接视为成功 + return nil + } + + cl, ok := cool.GetClientOnly(useid1) + if !ok || cl == nil { + // 目标服务器不在线,清理僵尸在线标记并视为成功 + _ = share.ShareManager.DeleteUserOnline(userid) + cool.DeleteClientOnly(useid1) + return nil + } + + resultCh := make(chan error, 1) + go func() { + resultCh <- cl.KickPerson(userid) // 实现指定服务器踢人 + }() + + select { + case callErr := <-resultCh: + if callErr == nil { + return nil + } + + // 调用失败后兜底:用户若已离线/切服/目标服不在线都算成功 + useid2, err2 := share.ShareManager.GetUserOnline(userid) + if err2 != nil || useid2 == 0 || useid2 != useid1 { + return nil + } + if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil { + _ = share.ShareManager.DeleteUserOnline(userid) + cool.DeleteClientOnly(useid2) + return nil + } + + // 仍在线则返回失败,不按成功处理 + return callErr + case <-time.After(kickForwardTimeout): + // 仅防止无限等待;超时不算成功 + useid2, err2 := share.ShareManager.GetUserOnline(userid) + if err2 != nil || useid2 == 0 || useid2 != useid1 { + return nil + } + if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil { + _ = share.ShareManager.DeleteUserOnline(userid) + cool.DeleteClientOnly(useid2) + return nil + } + + return fmt.Errorf("kick timeout, user still online: uid=%d server=%d", userid, useid2) + } +} + +// 注册logic服务器 +func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error { + fmt.Println("注册logic服务器", id, port) + + //TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug + revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx) + if !ok { + return fmt.Errorf("no reverse client") + } + t := config.NewServerService().GetServerID((id)) + + aa, ok := cool.GetClient(t.OnlineID, t.Port) + if ok && aa != nil { //如果已经存在且这个端口已经被存过 + aa.QuitSelf(0) + } + cool.AddClient(100000*id+port, &revClient) + + //Refurh() + return nil + +} + +func CServer() *jsonrpc.RPCServer { + // create a new server instance + rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler]("")) + + rpcServer.Register("", &ServerHandler{}) + + return rpcServer + +} + +var closer jsonrpc.ClientCloser + +func StartClient(id, port uint32, callback any) *struct { + Kick func(uint32) error + + RegisterLogic func(uint32, uint32) error +} { + //cool.Config.File.Domain = "127.0.0.1" + var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc" + + closer1, err := jsonrpc.NewMergeClient(context.Background(), + rpcaddr, "", []interface{}{ + &RPCClient, + }, nil, jsonrpc.WithClientHandler("", callback), + jsonrpc.WithReconnFun(func() { RPCClient.RegisterLogic(id, port) }), + ) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + //if port != 0 { //注册logic + defer RPCClient.RegisterLogic(id, port) + + //} + + closer = closer1 + + return &RPCClient +} + +// Setup RPCClient with reverse call handler +var RPCClient struct { + Kick func(uint32) error //踢人 + + RegisterLogic func(uint32, uint32) error + + // UserLogin func(int32, int32) error //用户登录事件 + // UserLogout func(int32, int32) error //用户登出事件 +} diff --git a/common/socket/ServerEvent.go b/common/socket/ServerEvent.go index cfe1f3d0a..71a71fe31 100644 --- a/common/socket/ServerEvent.go +++ b/common/socket/ServerEvent.go @@ -84,13 +84,11 @@ func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) { //logging.Infof("conn[%v] disconnected", c.RemoteAddr().String()) v, _ := c.Context().(*player.ClientData) - - v.LF.Close() - // v.LF.Close() - //close(v.MsgChan) - if v.Player != nil { - v.Player.Save() //保存玩家数据 - + if v != nil { + v.Close() + if v.Player != nil { + v.Player.Save() //保存玩家数据 + } } //} diff --git a/logic/controller/fight_boss野怪和地图怪.go b/logic/controller/fight_boss野怪和地图怪.go index d915a6fef..6a648852f 100644 --- a/logic/controller/fight_boss野怪和地图怪.go +++ b/logic/controller/fight_boss野怪和地图怪.go @@ -43,6 +43,7 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe ai := player.NewAI_player(monsterInfo) ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID) + ai.BossScript = bossConfigs[0].Script ai.Prop[0] = 2 var fightC *fight.FightC diff --git a/logic/controller/fight_塔.go b/logic/controller/fight_塔.go index e8369ff21..6c8788a6d 100644 --- a/logic/controller/fight_塔.go +++ b/logic/controller/fight_塔.go @@ -110,7 +110,7 @@ func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (resu result = &fight.S2C_ChoiceLevelRequestInfo{CurFightLevel: currentLevel} appendTowerNextBossPreview(&result.BossID, bossList) - monsterInfo, ok := buildTowerMonsterInfo(currentBoss) + monsterInfo, bossScript, ok := buildTowerMonsterInfo(currentBoss) if !ok { return nil, errorcode.ErrorCodes.ErrPokemonNotExists } @@ -119,6 +119,7 @@ func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (resu c.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC ai := player.NewAI_player(monsterInfo) + ai.BossScript = bossScript _, err = fight.NewFight(c, ai, c.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) { if foi.Reason != 0 || foi.WinnerId != c.Info.UserID { return @@ -195,10 +196,10 @@ func appendTowerNextBossPreview(dst *[]uint32, bossList []configmodel.BaseTowerC } } -func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.PlayerInfo, bool) { +func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.PlayerInfo, string, bool) { bosses := service.NewBossService().Get(towerBoss.BossIds[0]) if len(bosses) == 0 { - return nil, false + return nil, "", false } monsterInfo := &model.PlayerInfo{Nick: towerBoss.Name} @@ -234,7 +235,7 @@ func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.Player monsterInfo.PetList = append(monsterInfo.PetList, *monster) } - return monsterInfo, true + return monsterInfo, bosses[0].Script, true } func handleTowerFightWin(c *player.Player, cmd uint32, taskID int, currentLevel uint32) { diff --git a/logic/fight.test b/logic/fight.test new file mode 100755 index 000000000..842669c60 Binary files /dev/null and b/logic/fight.test differ diff --git a/logic/service/fight/input/ai.go b/logic/service/fight/input/ai.go index 83bab2bbe..9f7b0e5c0 100644 --- a/logic/service/fight/input/ai.go +++ b/logic/service/fight/input/ai.go @@ -2,6 +2,8 @@ package input import ( "blazing/logic/service/fight/info" + "blazing/logic/service/player" + configmodel "blazing/modules/config/model" "github.com/gogf/gf/v2/util/grand" ) @@ -22,6 +24,16 @@ func (our *Input) GetAction() { return t.HookAction() }) + + if aiPlayer, ok := our.Player.(*player.AI_player); ok && aiPlayer.BossScript != "" { + scriptBoss := &configmodel.BossConfig{Script: aiPlayer.BossScript} + nextByScript, err := scriptBoss.RunHookActionScript(next) + if err != nil { + return + } + next = nextByScript + } + if !next { return } diff --git a/logic/service/fight/loop.go b/logic/service/fight/loop.go index 044b340de..24e0a9953 100644 --- a/logic/service/fight/loop.go +++ b/logic/service/fight/loop.go @@ -162,7 +162,8 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{}) waitr := time.Duration(f.waittime)*time.Millisecond*10 + 30*time.Second - timeout := time.After(waitr) + timeout := time.NewTimer(waitr) + defer timeout.Stop() for len(actions) < len(expectedSlots) { select { @@ -266,11 +267,12 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{}) actions[key] = paction //fmt.Println("玩家执行动作:", pid, paction.Priority()) - case <-timeout: + case <-timeout.C: r := f.handleTimeout(expectedSlots, actions) if r { return flattenActionMap(actions) } + timeout.Reset(waitr) } } @@ -287,7 +289,7 @@ func (f *FightC) handleTimeout(expectedSlots map[actionSlotKey]struct{}, actions } player := f.getPlayerByID(key.PlayerID) if player != nil { - go f.UseSkillAt(player, 0, key.ActorIndex, 0) + f.UseSkillAt(player, 0, key.ActorIndex, 0) } } return false diff --git a/logic/service/player/Monster.go b/logic/service/player/Monster.go index db85648e2..c5f779d57 100644 --- a/logic/service/player/Monster.go +++ b/logic/service/player/Monster.go @@ -2,7 +2,8 @@ package player import ( "blazing/common/utils" - "blazing/modules/config/model" + configmodel "blazing/modules/config/model" + playermodel "blazing/modules/player/model" "sync/atomic" "time" @@ -10,15 +11,17 @@ import ( "github.com/samber/lo" ) -func (p *Player) IsMatch(t model.Event) bool { - _, ok := lo.Find(t.Weather, func(item int32) bool { - return item == int32(p.GetSpace().MapBossSInfo.Wer) - }) - if !ok { - // 不在同一天气下 - return false - +func (p *Player) IsMatch(t configmodel.Event) bool { + if len(t.Weather) > 0 { + _, ok := lo.Find(t.Weather, func(item int32) bool { + return item == int32(p.GetSpace().MapBossSInfo.Wer) + }) + if !ok { + // 不在同一天气下 + return false + } } + if t.StartTime != "" && t.EndTime != "" { ok, _ := utils.IsCurrentTimeInRange(t.StartTime, t.EndTime) if !ok { @@ -26,8 +29,74 @@ func (p *Player) IsMatch(t model.Event) bool { } } - return true + if len(t.Week) > 0 { + week := int32(time.Now().Weekday()) + if week == 0 { + week = 7 + } + _, ok := lo.Find(t.Week, func(item int32) bool { + return item == week + }) + if !ok { + return false + } + } + + if len(t.Sprites) > 0 && !matchPetIDInList(t.Sprites, p.Info.PetList, p.Info.BackupPetList) { + return false + } + + if len(t.FirstSprites) > 0 { + if len(p.Info.PetList) == 0 { + return false + } + + firstPetID := int32(p.Info.PetList[0].ID) + _, ok := lo.Find(t.FirstSprites, func(item int32) bool { + return item == firstPetID + }) + if !ok { + return false + } + } + + if len(t.MustTask) > 0 { + for _, taskID := range t.MustTask { + if p.Info.GetTask(int(taskID)) != playermodel.Completed { + return false + } + } + } + + if len(t.MustItem) > 0 { + if p.Service == nil || p.Service.Item == nil { + return false + } + for _, itemID := range t.MustItem { + if p.Service.Item.CheakItem(uint32(itemID)) <= 0 { + return false + } + } + } + + return true +} + +func matchPetIDInList(targetIDs []int32, petLists ...[]playermodel.PetInfo) bool { + for _, pets := range petLists { + for _, pet := range pets { + petID := int32(pet.ID) + _, ok := lo.Find(targetIDs, func(item int32) bool { + return item == petID + }) + if ok { + return true + } + } + } + + return false } // 应该根据怪物信息决定后端生成 diff --git a/logic/service/player/ai.go b/logic/service/player/ai.go index 05bd4ee69..aab9d3d79 100644 --- a/logic/service/player/ai.go +++ b/logic/service/player/ai.go @@ -4,5 +4,5 @@ type AI_player struct { baseplayer CanCapture int - + BossScript string } diff --git a/logic/service/player/pack.go b/logic/service/player/pack.go index 63c8dfe19..5e08f26ef 100644 --- a/logic/service/player/pack.go +++ b/logic/service/player/pack.go @@ -7,6 +7,7 @@ import ( "encoding/binary" "encoding/hex" "sync" + "sync/atomic" "context" @@ -102,6 +103,10 @@ func putPacketData(buf []byte) { } func (h *ClientData) PushEvent(v []byte, submit func(task func()) error) { + if h == nil || h.IsClosed() { + return + } + var header common.TomeeHeader header.Len = binary.BigEndian.Uint32(v[0:4]) header.CMD = binary.BigEndian.Uint32(v[5:9]) @@ -111,9 +116,18 @@ func (h *ClientData) PushEvent(v []byte, submit func(task func()) error) { copy(header.Data, v[17:]) } - _ = submit(func() { - h.LF.Producer().Write(header) + err := submit(func() { + if h.IsClosed() || h.LF == nil || !h.LF.Running() { + putPacketData(header.Data) + return + } + if err := h.LF.Producer().Write(header); err != nil { + putPacketData(header.Data) + } }) + if err != nil { + putPacketData(header.Data) + } } // 重写 @@ -241,6 +255,20 @@ type ClientData struct { Wsmsg *WsCodec Conn gnet.Conn LF *lockfree.Lockfree[common.TomeeHeader] + closed int32 +} + +func (p *ClientData) IsClosed() bool { + return atomic.LoadInt32(&p.closed) == 1 +} + +func (p *ClientData) Close() { + if !atomic.CompareAndSwapInt32(&p.closed, 0, 1) { + return + } + if p.LF != nil && p.LF.Running() { + _ = p.LF.Close() + } } func (p *ClientData) GetPlayer(userid uint32) *Player { //TODO 这里待优化,可能存在内存泄漏问题 diff --git a/logic/service/player/server.go b/logic/service/player/server.go index 3e8e5e4ab..0f8f85490 100644 --- a/logic/service/player/server.go +++ b/logic/service/player/server.go @@ -1,13 +1,16 @@ package player +import "blazing/common/data/share" + func KickPlayer(userid uint32) error { //踢出玩家 //TODO 返回错误码 //var player *entity.Player if player1, ok := Mainplayer.Load(userid); ok { player1.Player.Kick(false) - + return nil } - //return player + // 已不在本服在线列表,视为离线并清理僵尸在线标记 + _ = share.ShareManager.DeleteUserOnline(userid) return nil } diff --git a/logic/service/space/space.go b/logic/service/space/space.go index 1381a7232..24331c7ee 100644 --- a/logic/service/space/space.go +++ b/logic/service/space/space.go @@ -221,14 +221,15 @@ func (ret *Space) init() { } func (p *Space) IsMatch(t model.Event) bool { - _, ok := lo.Find(t.Weather, func(item int32) bool { - return item == int32(p.MapBossSInfo.Wer) - }) - if !ok { - - return false - + if len(t.Weather) > 0 { + _, ok := lo.Find(t.Weather, func(item int32) bool { + return item == int32(p.MapBossSInfo.Wer) + }) + if !ok { + return false + } } + if t.StartTime != "" && t.EndTime != "" { ok, _ := utils.IsCurrentTimeInRange(t.StartTime, t.EndTime) if !ok { @@ -236,6 +237,20 @@ func (p *Space) IsMatch(t model.Event) bool { } } + if len(t.Week) > 0 { + week := int32(time.Now().Weekday()) + if week == 0 { + week = 7 + } + + _, ok := lo.Find(t.Week, func(item int32) bool { + return item == week + }) + if !ok { + return false + } + } + return true } diff --git a/modules/config/model/boss_pet.go b/modules/config/model/boss_pet.go index 95a27ee54..ee38c4339 100644 --- a/modules/config/model/boss_pet.go +++ b/modules/config/model/boss_pet.go @@ -2,6 +2,10 @@ package model import ( "blazing/cool" + "fmt" + "strings" + + "github.com/dop251/goja" ) const ( @@ -46,3 +50,47 @@ func NewBossConfig() *BossConfig { func init() { cool.CreateTable(&BossConfig{}) } + +// RunHookActionScript 执行BOSS脚本中的 hookAction,并传入 fight 的 hookaction 参数。 +// 返回值遵循 HookAction 语义:true 允许继续出手,false 阻止继续出手。 +func (b *BossConfig) RunHookActionScript(hookAction any) (bool, error) { + if b == nil || strings.TrimSpace(b.Script) == "" { + return true, nil + } + + program, err := goja.Compile("boss_hook_action.js", b.Script, false) + if err != nil { + return false, fmt.Errorf("compile boss script: %w", err) + } + + vm := goja.New() + if _, err = vm.RunProgram(program); err != nil { + return false, fmt.Errorf("run boss script: %w", err) + } + + var ( + callable goja.Callable + ok bool + ) + for _, fnName := range []string{"hookAction", "HookAction", "hookaction"} { + callable, ok = goja.AssertFunction(vm.Get(fnName)) + if ok { + break + } + } + if !ok { + return false, fmt.Errorf("boss script function not found: hookAction") + } + + result, err := callable(goja.Undefined(), vm.ToValue(hookAction)) + if err != nil { + return false, fmt.Errorf("execute boss hookAction: %w", err) + } + + // 与既有HookAction默认行为保持一致:未显式返回时视为允许继续出手。 + if goja.IsUndefined(result) || goja.IsNull(result) { + return true, nil + } + + return result.ToBoolean(), nil +} diff --git a/modules/config/model/boss_pet_test.go b/modules/config/model/boss_pet_test.go new file mode 100644 index 000000000..1ed5d122e --- /dev/null +++ b/modules/config/model/boss_pet_test.go @@ -0,0 +1,44 @@ +package model + +import "testing" + +type testHookAction struct { + Allow bool + Round int +} + +func TestBossConfigRunHookActionScript(t *testing.T) { + boss := &BossConfig{ + Script: ` + function hookAction(hookaction) { + return hookaction.Allow && hookaction.Round >= 2; + } + `, + } + + ok, err := boss.RunHookActionScript(testHookAction{Allow: true, Round: 2}) + if err != nil { + t.Fatalf("RunHookActionScript returned error: %v", err) + } + if !ok { + t.Fatalf("RunHookActionScript = false, want true") + } +} + +func TestBossConfigRunHookActionScriptEmptyReturnDefaultsTrue(t *testing.T) { + boss := &BossConfig{ + Script: ` + function hookAction(hookaction) { + var _ = hookaction; + } + `, + } + + ok, err := boss.RunHookActionScript(testHookAction{Allow: false, Round: 1}) + if err != nil { + t.Fatalf("RunHookActionScript returned error: %v", err) + } + if !ok { + t.Fatalf("RunHookActionScript = false, want true") + } +} diff --git a/modules/player/service/info.go b/modules/player/service/info.go index 27b235489..c50731f2e 100644 --- a/modules/player/service/info.go +++ b/modules/player/service/info.go @@ -179,19 +179,51 @@ func (s *InfoService) Gensession() string { func (s *InfoService) Kick(id uint32) error { useid1, err := share.ShareManager.GetUserOnline(id) - - if err != nil { - return err + if err != nil || useid1 == 0 { + // 请求进入时已经离线,视为成功 + return nil } cl, ok := cool.GetClientOnly(useid1) - if ok { - err := cl.KickPerson(id) //实现指定服务器踢人 - if err != nil { - return err - } + if !ok || cl == nil { + // 目标服务器不在线,清理僵尸在线标记并视为成功 + _ = share.ShareManager.DeleteUserOnline(id) + return nil + } + + resultCh := make(chan error, 1) + go func() { + resultCh <- cl.KickPerson(id) // 实现指定服务器踢人 + }() + + select { + case callErr := <-resultCh: + if callErr == nil { + return nil + } + + // 调用失败后兜底:若已离线/切服/目标服不在线则视为成功 + useid2, err2 := share.ShareManager.GetUserOnline(id) + if err2 != nil || useid2 == 0 || useid2 != useid1 { + return nil + } + if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil { + _ = share.ShareManager.DeleteUserOnline(id) + return nil + } + return callErr + case <-time.After(3 * time.Second): + // 防止异常场景下无限等待;超时不按成功处理 + useid2, err2 := share.ShareManager.GetUserOnline(id) + if err2 != nil || useid2 == 0 || useid2 != useid1 { + return nil + } + if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil { + _ = share.ShareManager.DeleteUserOnline(id) + return nil + } + return fmt.Errorf("kick timeout, user still online: uid=%d server=%d", id, useid2) } - return nil } // saveToLocalFile 兜底保存:将数据写入本地lose文件夹