Files
bl/logic/service/player/player.go
昔念 eff23d5bd0 fix(login): 深拷贝用户信息以避免数据竞争
在登录逻辑中,使用 `copier.CopyWithOption` 进行深拷贝,确保发送给其他玩家的
玩家信息不会因为引用同一对象而导致数据异常。同时修正了部分字段注释和默认值说明。

feat(maps): 更新地图角色信息结构体字段描述

更新 `OutInfo` 结构体中的 `Action`、`Direction` 和 `ChangeShape` 字段的注释,
使其更准确地反映其用途和含义,便于后续维护与开发理解。

fix(player): 完善登录位置判断条件

在玩家完成登录时,除了判断 `MapID > 10000` 外,增加对 `MapID == 0` 的处理,
确保角色能正确被传送到默认地图。

refactor(walk): 移除无用上下文导入并优化日志记录

移除了未使用的 `context` 包导入,并调整了行走逻辑中的赋值顺序,使代码更清晰。
同时注释掉不再需要的调试日志输出语句。
2025-10-13 23:38:48 +08:00

395 lines
8.2 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/utils"
"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"
"fmt"
"sync"
"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"
)
func ConutPlayer() int {
count := 0
Mainplayer.Range(func(uint32, *Player) bool {
count++
return true // 继续遍历
})
return count
}
type ClientData struct {
IsCrossDomain bool //是否跨域过
Player *Player //客户实体
//UserID uint32
ERROR_CONNUT int
Wsmsg *WsCodec
}
func NewClientData() *ClientData {
cd := ClientData{
IsCrossDomain: false,
Player: nil,
Wsmsg: &WsCodec{},
}
return &cd
}
var Mainplayer = &utils.SyncMap[uint32, *Player]{} //玩家数据
func (c *Conn) SendPack(bytes []byte) error {
if t, ok := c.GetConn().Context().(*ClientData); ok {
if t.Wsmsg.Upgraded {
// This is the echo server
err := wsutil.WriteServerMessage(c.MainConn, ws.OpBinary, bytes)
if err != nil {
logging.Infof("conn[%v] [err=%v]", c.GetConn().RemoteAddr().String(), err.Error())
return err
}
} else {
_, err := c.GetConn().Write(bytes)
if err != nil {
glog.Debug(context.Background(), err)
}
}
}
return nil
}
type OgreInfo struct {
Data [9]OgrePetInfo
}
type OgrePetInfo struct {
Id uint32
Shiny uint32
Lv uint32 `struc:"skip"` //等级
}
type Player struct {
MainConn *Conn
baseplayer
IsLogin bool //是否登录
mu sync.Mutex
loginChan chan struct{} // 登录完成通知通道
StopChan timer.TimeNoder
context.Context
PVPinfo *info.PVPinfo //当前邀请的玩家ID
Onlinetime uint32 //当前登录时间
OgreInfo OgreInfo
Service *blservice.UserService
HavePVPinfo []*Player
monsters [3]int
Canmon bool //可以刷怪
Changemap bool //是否切换过地图
}
// PlayerOption 定义配置 Player 的函数类型
type PlayerOption func(*Player)
func WithConn(c *Conn) PlayerOption {
return func(p *Player) {
p.MainConn = c
}
}
func (p *Player) GetAction() {
}
// 刷怪具体实现
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) {
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
default:
ttt = append(ttt, v)
}
}
p.Service.ItemAdd(ttt...)
return
}
func (p *Player) SendPack(b []byte) error {
// fmt.Println("发送数据包", len(b))
err := p.MainConn.SendPack(b)
return err
}
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 {
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
}
// IsLoggedIn 检查是否已登录
func (lw *Player) IsLoggedIn() bool {
lw.mu.Lock()
defer lw.mu.Unlock()
return lw.IsLogin
}
// WaitForLogin 等待登录完成,无超时
func (lw *Player) WaitForLogin() error {
if lw.IsLoggedIn() {
return nil
}
// 阻塞等待登录完成
<-lw.loginChan
return nil
}
// WaitForLoginWithTimeout 带超时的登录等待
func (lw *Player) WaitForLoginWithTimeout(timeout time.Duration) error {
if lw.IsLoggedIn() {
return nil
}
// 使用定时器实现超时
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-lw.loginChan:
return nil
case <-timer.C:
return fmt.Errorf("登录等待超时: %v", timeout)
}
}
// WaitForLoginWithCtx 带上下文的登录等待
func (lw *Player) WaitForLoginWithCtx(ctx context.Context) error {
if lw.IsLoggedIn() {
return nil
}
select {
case <-lw.loginChan:
return nil
case <-ctx.Done():
return ctx.Err() // 上下文取消或超时
}
}
// CompleteLogin 标记登录完成并通知等待者
func (lw *Player) CompleteLogin() {
lw.mu.Lock()
defer lw.mu.Unlock()
if lw.Info.MapID > 10000 || lw.Info.MapID == 0 { //如果位于基地,就重置到传送仓
lw.Info.MapID = 1
}
if lw.IsNewPlayer() { //重置新手地图
lw.Info.MapID = 515
}
if !lw.IsLogin {
lw.IsLogin = true
close(lw.loginChan) // 关闭通道以通知所有等待者
}
}
// 定义检查函数判断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
}