Files
bl/logic/service/player/player.go
昔念 cccf26788e fix(socket): 玩家断开连接时增加保存锁,避免重复保存
在玩家断开连接时,使用 sync.Once 确保只保存一次玩家数据,
防止因并发或多次触发导致的数据异常。

feat(fight): 增加战斗资格判断与邀请取消功能

- 新增 Player.CanFight() 方法用于统一判断是否可以参与战斗
- 在多个战斗相关接口中加入 CanFight 检查
- 添加“取消战斗邀请”指令及处理逻辑(cmd: 2402)
- 修复部分错误码不准确的问题,提升提示一致性

refactor(login): 优化登录流程并增强健壮性

- 提前校验 session 合法性
- 增强获取玩家信息后的空指针检查
- 调整挖矿数据重置方式为 defer 执行
- 优化日志输出内容,便于调试追踪

docs(model): 更新部门、菜单等模型字段命名规范

将 orderNum 字段改为 ordernum,保持数据库列名风格一致,
同时更新了 base_sys_role 中 userId 为 userid。

perf(rate-limit): 提高登录接口的限流 Burst 容量

调整限流器配置,将请求 burst 容量从 2 提升至 5,
以应对短时间高频访问场景,改善用户体验。

chore(build): 忽略新增编译产物和临时文件

在 .gitignore 中添加 logic/logic2、login/login 等新生成文件路径,
避免误提交二进制文件到版本控制。
2025-10-31 00:53:22 +08:00

404 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package player
import (
"blazing/common/data/share"
"blazing/common/data/xmlres"
"blazing/common/socket/errorcode"
"blazing/common/utils"
"blazing/cool"
"fmt"
"math/rand"
"strings"
"blazing/logic/service/common"
"blazing/logic/service/fight/info"
"blazing/logic/service/space"
"blazing/modules/blazing/model"
blservice "blazing/modules/blazing/service"
"context"
"time"
"github.com/antlabs/timer"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/util/gconv"
"github.com/panjf2000/gnet/pkg/logging"
"github.com/panjf2000/gnet/v2"
)
func ConutPlayer() int {
count := 0
Mainplayer.Range(func(uint32, *Player) bool {
count++
return true // 继续遍历
})
return count
}
var Mainplayer = &utils.SyncMap[uint32, *Player]{} //玩家数据
type OgreInfo struct {
Data [9]OgrePetInfo
}
type OgrePetInfo struct {
Id uint32
Shiny uint32
Lv uint32 `struc:"skip"` //等级
}
type Player struct {
MainConn gnet.Conn
baseplayer
IsLogin bool //是否登录
StopChan timer.TimeNoder
context.Context
PVPinfo *info.PVPinfo //当前邀请的玩家ID
Onlinetime uint32 //当前登录时间
OgreInfo OgreInfo
Service *blservice.UserService
// PVP被邀请信息
HavePVPinfo []*Player
monsters [3]int
Canmon bool //可以刷怪
Changemap bool //是否切换过地图
}
// PlayerOption 定义配置 Player 的函数类型
type PlayerOption func(*Player)
func WithConn(c gnet.Conn) PlayerOption {
return func(p *Player) {
p.MainConn = c
}
}
func (p *Player) GetAction() {
}
func (p *Player) CanFight() bool {
if p.FightC != nil {
return false
}
for _, v := range p.Info.PetList {
if v.Hp > 0 { // 只要找到一个血量大于0的宠物就可以战斗
return true
}
}
// 遍历完所有宠物都没有血量大于0的才不能战斗
return false
}
// 刷怪具体实现
func (p *Player) SpawnMonsters() {
// 获取当前地图的怪物配置
// 创建数据包
tt := NewTomeeHeader(2004, p.Info.UserID)
p.genMonster(p.Info.MapID) //生成野怪
p.SendPack(tt.Pack(&p.OgreInfo))
}
// 2. 从 string 类型 slice 随机选一个元素
func RandomStringFromSlice(s []string) string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
randomIdx := r.Intn(len(s))
return s[randomIdx]
}
// 应该根据怪物信息决定后端生成
func (p *Player) genMonster(mapid uint32) {
var oldnum, newNum int
p.monsters, oldnum, newNum = replaceOneNumber(p.monsters)
// 设置怪物信息
t1 := OgreInfo{}
mapss, ok := xmlres.MonsterMap[gconv.Int(mapid)]
if ok && mapss.Monsters != nil {
for i, m := range mapss.Monsters.Monsters { //这里是9个
id := strings.Split(m.ID, " ")
lv := strings.Split(m.Lv, " ")
ttt := OgrePetInfo{
Id: gconv.Uint32(RandomStringFromSlice(id)),
}
if ttt.Id != 0 {
ttt.Shiny = 0 //待确认是否刷新异色
ttt.Lv = gconv.Uint32(RandomStringFromSlice(lv))
}
t1.Data[i] = ttt
}
}
if p.Changemap {
p.Changemap = false
p.OgreInfo = OgreInfo{} //切地图清空
for i := 0; i < 3; i++ {
p.OgreInfo.Data[p.monsters[i]] = t1.Data[p.monsters[i]]
}
} else {
p.OgreInfo.Data[oldnum] = OgrePetInfo{}
p.OgreInfo.Data[newNum] = t1.Data[newNum]
}
}
// 生成0-9之间三个不重复的随机数 进地图5s
func generateThreeUniqueNumbers() [3]int {
rand.Seed(time.Now().UnixNano())
selected := make(map[int]bool)
var result [3]int
index := 0
for index < 3 {
num := rand.Intn(9)
if !selected[num] {
selected[num] = true
result[index] = num
index++
}
}
return result
}
// 从三个数字中移除一个并从剩余6个数字中选一个补充 10s
func replaceOneNumber(original [3]int) ([3]int, int, int) {
// 随机选择要移除的索引0-2
removeIndex := rand.Intn(3)
removedNum := original[removeIndex]
// 找出所有不在原始数组中的数字(候选数字)
candidates := []int{}
originalMap := make(map[int]bool)
for _, num := range original {
originalMap[num] = true
}
for i := 0; i < 8; i++ {
if !originalMap[i] {
candidates = append(candidates, i)
}
}
// 从候选数字中随机选择一个
newNum := candidates[rand.Intn(len(candidates))]
// 创建新数组并替换数字
newNumbers := original
newNumbers[removeIndex] = newNum
return newNumbers, removedNum, newNum
}
// 添加物品
func (p *Player) ItemAdd(t ...model.SingleItemInfo) (result []model.SingleItemInfo) {
var ttt []model.SingleItemInfo
for _, v := range t {
switch v.ItemId {
case 1: //塞尔豆
p.Info.Coins = p.Info.Coins + v.ItemCnt
case 3: //累计经验
p.Info.ExpPool = p.Info.ExpPool + v.ItemCnt
case 5: //金豆ItemAdd
p.Info.GoldBean = p.Info.GoldBean + v.ItemCnt
default:
ttt = append(ttt, v)
}
}
p.Service.Item(func(rer map[uint32]model.SingleItemInfo) bool {
for _, v := range ttt {
itemx, ok := xmlres.ItemsMAP[int(v.ItemId)]
if !ok {
cool.Loger.Error(context.TODO(), "物品不存在", v.ItemId)
t1 := NewTomeeHeader(2601, p.Info.UserID)
t1.Result = uint32(errorcode.ErrorCodes.ErrBaseItemTypeLimit)
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
continue
}
itemm, ok := rer[v.ItemId]
if !ok {
rer[v.ItemId] = v
result = append(result, v)
continue
}
itemm.ItemCnt += v.ItemCnt
if itemm.ItemCnt > uint32(itemx.Max) {
cool.Loger.Error(context.TODO(), "物品超过拥有最大限制", v.ItemId)
t1 := NewTomeeHeader(2601, p.Info.UserID)
t1.Result = uint32(errorcode.ErrorCodes.ErrTooManyOfItem)
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
continue
}
result = append(result, v)
rer[v.ItemId] = itemm
}
return true
})
return
}
func (player1 *Player) Kick() {
if player1.IsLogin {
//取成功,否则创建
//player1.Save() //先保存数据再返回
head := NewTomeeHeader(1001, player1.Info.UserID)
head.Result = uint32(errorcode.ErrorCodes.ErrAccountLoggedInElsewhere)
//实际上这里有个问题,会造成重复保存问题
player1.SendPack(head.Pack(nil))
player1.MainConn.Context().(*ClientData).CloseChan = make(chan struct{})
player1.MainConn.Context().(*ClientData).Mu.Lock()
player1.MainConn.Close()
player1.MainConn.Context().(*ClientData).Mu.Unlock()
// clientdata.Player = player
<-player1.MainConn.Context().(*ClientData).CloseChan
}
}
func (p *Player) SendPack(b []byte) error {
if _, ok := p.MainConn.Context().(*ClientData); !ok {
return fmt.Errorf("链接错误,取消发包")
}
p.MainConn.Context().(*ClientData).Mu.Lock()
defer p.MainConn.Context().(*ClientData).Mu.Unlock()
if p.MainConn.Context().(*ClientData).Wsmsg.Upgraded {
// This is the echo server
err := wsutil.WriteServerMessage(p.MainConn, ws.OpBinary, b)
if err != nil {
logging.Infof("conn[%v] [err=%v]", p.MainConn.RemoteAddr().String(), err.Error())
return err
}
} else {
err := p.MainConn.AsyncWrite(b, nil)
if err != nil {
glog.Debug(context.Background(), err)
}
}
return nil
}
func (p *Player) Cheak(b error) {
if b != nil {
g.Log().Error(context.Background(), "出现错误", p.Info.UserID, b.Error())
}
}
func LeaveMap(c common.PlayerI) {
if c == nil {
return
}
if c.GetInfo() == nil {
return
}
if c.GetInfo().MapID == 0 {
return
}
t := NewTomeeHeader(2002, c.GetInfo().UserID)
for k, v := range space.GetSpace(c.GetInfo().MapID).User.Items() {
if k != c.GetInfo().UserID {
v.SendPack(t.Pack(&space.LeaveMapOutboundInfo{UserID: c.GetInfo().UserID}))
}
}
space.GetSpace(c.GetInfo().MapID).User.Remove(c.GetInfo().UserID)
}
// Save 保存玩家数据
func (p *Player) Save() {
if p.Info == nil {
return
}
if p.FightC != nil {
go func() {
defer func() {
if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值
// 1. 打印错误信息
cool.Loger.Error(context.TODO(), "panic 错误:", err)
}
}()
p.FightC.Over(p, info.BattleOverReason.PlayerOffline) //玩家逃跑,但是不能锁线程
}()
}
p.Info.TimeToday = p.Info.TimeToday + uint32(time.Now().Unix()) - uint32(p.Onlinetime) //保存电池时间
p.Onlinetime = uint32(time.Now().Unix())
p.Service.Save(p.Info)
LeaveMap(p)
p.StopChan.Stop() //停止刷怪
p.IsLogin = false
Mainplayer.Delete(p.Info.UserID)
share.ShareManager.DeleteUserOnline(p.Info.UserID) //设置用户登录服务器
}
// 是否可以获得经验
func (p *Player) CanGetExp() bool {
ttt := p.Info.TimeLimit - p.Info.TimeToday
return (uint32(time.Now().Unix()) - uint32(p.Onlinetime)) <= ttt
}
// CompleteLogin 标记登录完成并通知等待者
func (lw *Player) CompleteLogin() {
if lw.Info.MapID > 500 || lw.Info.MapID == 0 { //如果位于基地,就重置到传送仓
lw.Info.MapID = 1
}
if lw.IsNewPlayer() { //重置新手地图
lw.Info.MapID = 515
}
lw.IsLogin = true
}
// 定义检查函数判断84-87索引中是否有任意一个元素不等于3
func (lw *Player) IsNewPlayer() bool {
// 遍历84到87的索引
for i := 84; i <= 87; i++ {
if lw.Info.TaskList[i] != 3 {
return true // 只要有一个不等于3就返回true
}
}
return false // 全部等于3则返回false
}