diff --git a/.ide/Dockerfile b/.ide/Dockerfile index efdfbd9c4..9efd2d3bc 100644 --- a/.ide/Dockerfile +++ b/.ide/Dockerfile @@ -18,9 +18,11 @@ ENV GOMODCACHE=/workspace/.cache/gomod # ========================================== # 2. Codex 配置 (更换时修改这里,重新 build) # ========================================== -ENV CODEX_BASE_URL="https://fastai.fast" +ENV CODEX_BASE_URL="https://www.jnm.lol/v1" + ENV CODEX_MODEL="gpt-5.4" -ENV OPENAI_API_KEY="sk-63e660ffa319cac900c17185c69282c1f62403f04717334fc8f492d1816bf657" + +ENV OPENAI_API_KEY="pk_live__NQFz14yuraSLUY9mXCuQ2Swh1NM9XV4uVOB1qukipw" # ========================================== # 3. 安装系统依赖、Golang、Code-server diff --git a/.ide/help.md b/.ide/help.md index f891868f2..7812952e2 100644 --- a/.ide/help.md +++ b/.ide/help.md @@ -26,3 +26,10 @@ kuaipao.ai 充了十块 cjf19970621 cjf19970621 + + +fastai.fast 575560454@qq.com 575560454 + + + + diff --git a/common/cool/global.go b/common/cool/global.go index 929027b10..95affc3ae 100644 --- a/common/cool/global.go +++ b/common/cool/global.go @@ -22,8 +22,14 @@ var ctx = context.TODO() type Cmd struct { Func reflect.Value //方法函数 Req reflect.Type //请求体 + // HeaderFieldIndex 是请求结构体中 TomeeHeader 字段的索引路径。 + HeaderFieldIndex []int + // UseConn 标记第二个参数是否为 gnet.Conn。 + UseConn bool // 新增:预缓存的req创建函数(返回结构体指针) NewReqFunc func() interface{} + // NewReqValue 返回请求结构体指针的 reflect.Value,避免重复构造类型信息。 + NewReqValue func() reflect.Value //Res reflect.Value //返回体 } 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/data/Element/element.go b/common/data/Element/element.go index 711d0f9c6..412f450a8 100644 --- a/common/data/Element/element.go +++ b/common/data/Element/element.go @@ -42,15 +42,15 @@ const ( maxMatrixSize = 227 // 矩阵维度(覆盖最大属性ID 226) ) -// 合法单属性ID集合(快速校验) -var validSingleElementIDs = map[int]bool{ +// 合法单属性ID集合(按ID直接索引,避免运行时 map 查找) +var validSingleElementIDs = [maxMatrixSize]bool{ 1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true, 11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true, 221: true, 222: true, 223: true, 224: true, 225: true, 226: true, } -// 元素名称映射(全属性对应,便于日志输出) -var elementNameMap = map[ElementType]string{ +// 元素名称映射(按ID直接索引,便于日志输出) +var elementNameMap = [maxMatrixSize]string{ ElementTypeGrass: "GRASS", ElementTypeWater: "WATER", ElementTypeFire: "FIRE", @@ -198,46 +198,55 @@ type ElementCombination struct { ID int // 组合唯一ID } -// 全局预加载资源(程序启动时init初始化,运行时直接使用) +// 全局预加载资源(程序启动时初始化,运行时只读) var ( - // 元素组合池:key=组合ID,value=组合实例(预加载所有合法组合) - elementCombinationPool = make(map[int]*ElementCombination, 150) // 128双+26单=154,预分配足够容量 - // 单属性克制矩阵(预初始化所有特殊克制关系,默认1.0) - matrix [maxMatrixSize][maxMatrixSize]float64 + validCombinationIDs [maxMatrixSize]bool + elementCombinationPool [maxMatrixSize]ElementCombination + dualElementSecondaryPool [maxMatrixSize]ElementType + matrix [maxMatrixSize][maxMatrixSize]float64 + Calculator *ElementCalculator ) // init 预加载所有资源(程序启动时执行一次,无并发问题) func init() { - // 1. 初始化单属性克制矩阵 initFullTableMatrix() + initElementCombinationPool() + Calculator = NewElementCalculator() +} - // 2. 预加载所有单属性组合 - for id := range validSingleElementIDs { - combo := &ElementCombination{ - Primary: ElementType(id), - Secondary: nil, - ID: id, +func initElementCombinationPool() { + for id, valid := range validSingleElementIDs { + if !valid { + continue + } + validCombinationIDs[id] = true + elementCombinationPool[id] = ElementCombination{ + Primary: ElementType(id), + ID: id, } - elementCombinationPool[id] = combo } - // 3. 预加载所有双属性组合 for dualID, atts := range dualElementMap { primaryID, secondaryID := atts[0], atts[1] - // 按ID升序排序,保证组合一致性 primary, secondary := ElementType(primaryID), ElementType(secondaryID) if primary > secondary { primary, secondary = secondary, primary } - combo := &ElementCombination{ + + dualElementSecondaryPool[dualID] = secondary + validCombinationIDs[dualID] = true + elementCombinationPool[dualID] = ElementCombination{ Primary: primary, - Secondary: &secondary, + Secondary: &dualElementSecondaryPool[dualID], ID: dualID, } - elementCombinationPool[dualID] = combo } } +func isValidCombinationID(id int) bool { + return id > 0 && id < maxMatrixSize && validCombinationIDs[id] +} + // IsDual 判断是否为双属性 func (ec *ElementCombination) IsDual() bool { return ec.Secondary != nil @@ -245,84 +254,82 @@ func (ec *ElementCombination) IsDual() bool { // Elements 获取所有属性列表 func (ec *ElementCombination) Elements() []ElementType { - if ec.IsDual() { - return []ElementType{ec.Primary, *ec.Secondary} + if secondary := ec.Secondary; secondary != nil { + return []ElementType{ec.Primary, *secondary} } return []ElementType{ec.Primary} } // String 友好格式化输出 func (ec *ElementCombination) String() string { - primaryName := elementNameMap[ec.Primary] - if !ec.IsDual() { - return fmt.Sprintf("(%s)", primaryName) + if secondary := ec.Secondary; secondary != nil { + return fmt.Sprintf("(%s, %s)", elementNameMap[ec.Primary], elementNameMap[*secondary]) } - return fmt.Sprintf("(%s, %s)", primaryName, elementNameMap[*ec.Secondary]) + return fmt.Sprintf("(%s)", elementNameMap[ec.Primary]) } -// ElementCalculator 无锁元素克制计算器(依赖预加载资源) +// ElementCalculator 无锁元素克制计算器(所有倍数在初始化阶段预计算) type ElementCalculator struct { - offensiveCache map[string]float64 // 攻击克制缓存(运行时填充,无并发写) + offensiveTable [maxMatrixSize][maxMatrixSize]float64 } -// NewElementCalculator 创建计算器实例(仅初始化缓存) +// NewElementCalculator 创建计算器实例(构建只读查表缓存) func NewElementCalculator() *ElementCalculator { - return &ElementCalculator{ - offensiveCache: make(map[string]float64, 4096), // 预分配大容量缓存 + c := &ElementCalculator{} + c.initOffensiveTable() + return c +} + +func (c *ElementCalculator) initOffensiveTable() { + for attackerID, valid := range validCombinationIDs { + if !valid { + continue + } + attacker := &elementCombinationPool[attackerID] + for defenderID, valid := range validCombinationIDs { + if !valid { + continue + } + defender := &elementCombinationPool[defenderID] + c.offensiveTable[attackerID][defenderID] = c.calculateMultiplier(attacker, defender) + } } } // getMatrixValue 直接返回矩阵值(修复核心问题:不再将0转换为1) func (c *ElementCalculator) getMatrixValue(attacker, defender ElementType) float64 { - return matrix[attacker][defender] // 矩阵默认已初始化1.0,特殊值直接返回 + return matrix[attacker][defender] } -// GetCombination 获取元素组合(直接从预加载池读取) +// GetCombination 获取元素组合(直接按ID索引) func (c *ElementCalculator) GetCombination(id int) (*ElementCombination, error) { - combo, exists := elementCombinationPool[id] - if !exists { + if !isValidCombinationID(id) { return nil, fmt.Errorf("invalid element combination ID: %d", id) } - return combo, nil + return &elementCombinationPool[id], nil } -// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(缓存优先) +// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(只读查表) func (c *ElementCalculator) GetOffensiveMultiplier(attackerID, defenderID int) (float64, error) { - // 1. 获取预加载的组合实例 - attacker, err := c.GetCombination(attackerID) - if err != nil { - return 0, fmt.Errorf("attacker invalid: %w", err) + if !isValidCombinationID(attackerID) { + return 0, fmt.Errorf("attacker invalid: invalid element combination ID: %d", attackerID) } - defender, err := c.GetCombination(defenderID) - if err != nil { - return 0, fmt.Errorf("defender invalid: %w", err) + if !isValidCombinationID(defenderID) { + return 0, fmt.Errorf("defender invalid: invalid element combination ID: %d", defenderID) } - - // 2. 缓存键(全局唯一) - cacheKey := fmt.Sprintf("a%d_d%d", attackerID, defenderID) - if val, exists := c.offensiveCache[cacheKey]; exists { - return val, nil - } - - // 3. 核心计算+缓存 - val := c.calculateMultiplier(attacker, defender) - c.offensiveCache[cacheKey] = val - return val, nil + return c.offensiveTable[attackerID][defenderID], nil } // calculateMultiplier 核心克制计算逻辑 func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombination) float64 { - // 场景1:单→单 if !attacker.IsDual() && !defender.IsDual() { return c.getMatrixValue(attacker.Primary, defender.Primary) } - // 场景2:单→双 if !attacker.IsDual() { y1, y2 := defender.Primary, *defender.Secondary m1 := c.getMatrixValue(attacker.Primary, y1) m2 := c.getMatrixValue(attacker.Primary, y2) - switch { case m1 == 2 && m2 == 2: return 4.0 @@ -333,12 +340,10 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi } } - // 场景3:双→单 if !defender.IsDual() { return c.calculateDualToSingle(attacker.Primary, *attacker.Secondary, defender.Primary) } - // 场景4:双→双 x1, x2 := attacker.Primary, *attacker.Secondary y1, y2 := defender.Primary, *defender.Secondary coeffY1 := c.calculateDualToSingle(x1, x2, y1) @@ -350,7 +355,6 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender ElementType) float64 { k1 := c.getMatrixValue(attacker1, defender) k2 := c.getMatrixValue(attacker2, defender) - switch { case k1 == 2 && k2 == 2: return 4.0 @@ -361,60 +365,49 @@ func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender } } -var Calculator = NewElementCalculator() - // TestAllScenarios 全场景测试(验证预加载和计算逻辑) func TestAllScenarios() { - - // 测试1:单→单(草→水) m1, _ := Calculator.GetOffensiveMultiplier(1, 2) fmt.Println("草→水: %.2f(预期2.0)", m1) if math.Abs(m1-2.0) > 0.001 { fmt.Println("测试1失败:实际%.2f", m1) } - // 测试2:特殊单→单(混沌→虚空) m2, _ := Calculator.GetOffensiveMultiplier(222, 226) fmt.Println("混沌→虚空: %.2f(预期0.0)", m2) if math.Abs(m2-0.0) > 0.001 { fmt.Println("测试2失败:实际%.2f", m2) } - // 测试3:单→双(火→冰龙(43)) m3, _ := Calculator.GetOffensiveMultiplier(3, 43) fmt.Println("火→冰龙: %.2f(预期1.5)", m3) if math.Abs(m3-1.5) > 0.001 { fmt.Println("测试3失败:实际%.2f", m3) } - // 测试4:双→特殊单(混沌暗影(92)→神灵(223)) m4, _ := Calculator.GetOffensiveMultiplier(92, 223) fmt.Println("混沌暗影→神灵: %.2f(预期1.25)", m4) if math.Abs(m4-1.25) > 0.001 { fmt.Println("测试4失败:实际%.2f", m4) } - // 测试5:双→双(虚空邪灵(113)→混沌远古(98)) m5, _ := Calculator.GetOffensiveMultiplier(113, 98) fmt.Println("虚空邪灵→混沌远古: %.2f(预期0.875", m5) if math.Abs(m5-0.875) > 0.001 { fmt.Println("测试5失败:实际%.2f", m5) } - // 测试6:缓存命中 m6, _ := Calculator.GetOffensiveMultiplier(113, 98) if math.Abs(m6-m5) > 0.001 { fmt.Println("测试6失败:缓存未命中") } - // 测试7:含无效组合(电→地面) m7, _ := Calculator.GetOffensiveMultiplier(5, 7) fmt.Println("电→地面: %.2f(预期0.0)", m7) if math.Abs(m7-0.0) > 0.001 { fmt.Println("测试7失败:实际%.2f", m7) } - // 测试8:双属性含无效(电战斗→地面) m8, _ := Calculator.GetOffensiveMultiplier(35, 7) fmt.Println("电战斗→地面: %.2f(预期0.25)", m8) if math.Abs(m8-0.25) > 0.001 { 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 e98b502c8..ad35cd0d1 100644 --- a/common/socket/ServerEvent.go +++ b/common/socket/ServerEvent.go @@ -1,24 +1,29 @@ package socket import ( + "blazing/common/socket/codec" + "blazing/cool" + "blazing/logic/service/player" + "blazing/modules/config/service" + "bytes" "context" "encoding/binary" "errors" - "io" "log" "os" "sync/atomic" "time" - "blazing/cool" - "blazing/logic/service/player" - "blazing/modules/config/service" - "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "github.com/panjf2000/gnet/v2" ) +const ( + minPacketLen = 17 + maxPacketLen = 10 * 1024 +) + func (s *Server) Boot(serverid, port uint32) error { // go s.bootws() s.serverid = serverid @@ -53,7 +58,6 @@ func (s *Server) Stop() error { func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) { defer func() { if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值 - // 1. 打印错误信息 if t, ok := c.Context().(*player.ClientData); ok { if t.Player != nil { if t.Player.Info != nil { @@ -62,53 +66,35 @@ func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) { } } - } else { cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, err) - } - } }() - // 识别 RST 导致的连接中断(错误信息含 "connection reset") - // if err != nil && (strings.Contains(err.Error(), "connection reset") || strings.Contains(err.Error(), "reset by peer")) { - // remoteIP := c.RemoteAddr().(*net.TCPAddr).IP.String() - // log.Printf("RST 攻击检测: 来源 %s, 累计攻击次数 %d", remoteIP) - - // // 防护逻辑:临时封禁异常 IP(可扩展为 IP 黑名单) - // // go s.tempBlockIP(remoteIP, 5*time.Minute) - // } - //fmt.Println(err, c.RemoteAddr().String(), "断开连接") atomic.AddInt64(&cool.Connected, -1) - //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 { - go v.Player.SaveOnDisconnect() //保存玩家数据 - + if v != nil { + v.Close() + if v.Player != nil { + v.Player.Save() //保存玩家数据 + } } - - //} - //关闭连接 return } + func (s *Server) OnTick() (delay time.Duration, action gnet.Action) { g.Log().Async().Info(context.Background(), gtime.Now().ISO8601(), "服务器ID", cool.Config.ServerInfo.OnlineID, "链接数", atomic.LoadInt64(&cool.Connected)) if s.quit && atomic.LoadInt64(&cool.Connected) == 0 { - //执行正常退出逻辑 os.Exit(0) } return 30 * time.Second, gnet.None } + func (s *Server) OnBoot(eng gnet.Engine) gnet.Action { s.eng = eng - - service.NewServerService().SetServerID(s.serverid, s.port) //设置当前服务器端口 + service.NewServerService().SetServerID(s.serverid, s.port) return gnet.None } @@ -116,59 +102,52 @@ func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) { if s.network != "tcp" { return nil, gnet.Close } - if conn.Context() == nil { - conn.SetContext(player.NewClientData(conn)) //注入data + conn.SetContext(player.NewClientData(conn)) } - atomic.AddInt64(&cool.Connected, 1) - return nil, gnet.None } func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) { defer func() { - if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值 - // 1. 打印错误信息 + if err := recover(); err != nil { if t, ok := c.Context().(*player.ClientData); ok { - if t.Player != nil { - if t.Player.Info != nil { - cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err) - t.Player.Service.Info.Save(*t.Player.Info) - - } - + if t.Player != nil && t.Player.Info != nil { + cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err) + t.Player.Service.Info.Save(*t.Player.Info) } - } - } }() ws := c.Context().(*player.ClientData).Wsmsg - if ws.Tcp { //升级失败时候防止缓冲区溢出 + if ws.Tcp { return s.handleTCP(c) - } - tt, len1 := ws.ReadBufferBytes(c) - if tt == gnet.Close { - + readAction, inboundLen := ws.ReadBufferBytes(c) + if readAction == gnet.Close { return gnet.Close } - ok, action := ws.Upgrade(c) - if action != gnet.None { //连接断开 + state, action := ws.Upgrade(c) + if action != gnet.None { return action } - if !ok { //升级失败,说明是tcp连接 - ws.Tcp = true - - return s.handleTCP(c) - + if state == player.UpgradeNeedMoreData { + return gnet.None + } + if state == player.UpgradeUseTCP { + return s.handleTCP(c) + } + + if inboundLen > 0 { + if _, err := c.Discard(inboundLen); err != nil { + return gnet.Close + } + ws.ResetInboundMirror() } - // fmt.Println(ws.Buf.Bytes()) - c.Discard(len1) messages, err := ws.Decode(c) if err != nil { @@ -179,91 +158,93 @@ func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) { } for _, msg := range messages { - - s.onevent(c, msg.Payload) - //t.OnEvent(msg.Payload) + if !s.onevent(c, msg.Payload) { + return gnet.Close + } } - return gnet.None } -const maxBodyLen = 10 * 1024 // 业务最大包体长度,按需调整 func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) { + client := conn.Context().(*player.ClientData) + if s.discorse && !client.IsCrossDomainChecked() { + handled, ready, action := handle(conn) + if action != gnet.None { + return action + } + if !ready { + return gnet.None + } + if handled { + client.MarkCrossDomainChecked() + return gnet.None + } + client.MarkCrossDomainChecked() + } - conn.Context().(*player.ClientData).IsCrossDomain.Do(func() { //跨域检测 - handle(conn) - }) - - // handle(c) - // 先读取4字节的包长度 - lenBuf, err := conn.Peek(4) - + body, err := s.codec.Decode(conn) if err != nil { - if errors.Is(err, io.ErrShortBuffer) { - return + if errors.Is(err, codec.ErrIncompletePacket) { + return gnet.None } return gnet.Close } - - bodyLen := binary.BigEndian.Uint32(lenBuf) - - if bodyLen > maxBodyLen { + if !s.onevent(conn, body) { return gnet.Close } - - if conn.InboundBuffered() < int(bodyLen) { - return - } - // 提取包体 - body, err := conn.Next(int(bodyLen)) - if err != nil { - if errors.Is(err, io.ErrShortBuffer) { - return - } - return gnet.Close - } - - s.onevent(conn, body) - if conn.InboundBuffered() > 0 { - if err := conn.Wake(nil); err != nil { // wake up the connection manually to avoid missing the leftover data - + if err := conn.Wake(nil); err != nil { return gnet.Close } } return action - } -// CROSS_DOMAIN 定义跨域策略文件内容 const CROSS_DOMAIN = "\x00" - -// TEXT 定义跨域请求的文本格式 const TEXT = "\x00" -func handle(c gnet.Conn) { +func handle(c gnet.Conn) (handled bool, ready bool, action gnet.Action) { + probeLen := c.InboundBuffered() + if probeLen == 0 { + return false, false, gnet.None + } + if probeLen > len(TEXT) { + probeLen = len(TEXT) + } - // 读取数据并检查是否为跨域请求 - data, err := c.Peek(len(TEXT)) + data, err := c.Peek(probeLen) if err != nil { log.Printf("Error reading cross-domain request: %v", err) - return + return false, false, gnet.Close } - - if string(data) == TEXT { //判断是否是跨域请求 - //log.Printf("Received cross-domain request from %s", c.RemoteAddr()) - // 处理跨域请求 - c.Write([]byte(CROSS_DOMAIN)) - c.Discard(len(TEXT)) - - return + if !bytes.Equal(data, []byte(TEXT[:probeLen])) { + return false, true, gnet.None } - - //return + if probeLen < len(TEXT) { + return false, false, gnet.None + } + if _, err := c.Write([]byte(CROSS_DOMAIN)); err != nil { + return false, true, gnet.Close + } + if _, err := c.Discard(len(TEXT)); err != nil { + return false, true, gnet.Close + } + return true, true, gnet.None } -func (s *Server) onevent(c gnet.Conn, v []byte) { +func (s *Server) onevent(c gnet.Conn, v []byte) bool { + if !isValidPacket(v) { + return false + } if t, ok := c.Context().(*player.ClientData); ok { t.PushEvent(v, s.workerPool.Submit) } + return true +} + +func isValidPacket(v []byte) bool { + if len(v) < minPacketLen || len(v) > maxPacketLen { + return false + } + return binary.BigEndian.Uint32(v[0:4]) == uint32(len(v)) } diff --git a/docs/boss-script-hookaction-guide-2026-04-05.md b/docs/boss-script-hookaction-guide-2026-04-05.md new file mode 100644 index 000000000..7751dd23f --- /dev/null +++ b/docs/boss-script-hookaction-guide-2026-04-05.md @@ -0,0 +1,183 @@ +# Boss Script(HookAction)接入说明 + +日期:2026-04-05 + +## 1. 执行流程 + +1. 先执行战斗效果链 `HookAction()` +2. 执行脚本 `hookAction(hookaction)` +3. 用脚本返回值决定是否继续出手 +4. 脚本可直接调用 Go 绑定函数:`useSkill()`、`switchPet()` + +## 2. JS 可调用的 Go 函数 + +1. `useSkill(skillId: number)` +2. `switchPet(catchTime: number)` + +## 3. `hookaction` 参数字段 + +基础字段: + +1. `hookaction.hookaction: boolean` +2. `hookaction.round: number` +3. `hookaction.is_first: boolean` +4. `hookaction.our: { pet_id, catch_time, hp, max_hp } | null` +5. `hookaction.opp: { pet_id, catch_time, hp, max_hp } | null` +6. `hookaction.skills: Array<{ skill_id, pp, can_use }>` + +AttackValue 映射字段(重点): + +1. `hookaction.our_attack` +2. `hookaction.opp_attack` + +结构: + +```ts +{ + skill_id: number; + attack_time: number; + is_critical: number; + lost_hp: number; + gain_hp: number; + remain_hp: number; + max_hp: number; + state: number; + offensive: number; + status: number[]; // 对应 AttackValue.Status[20] + prop: number[]; // 对应 AttackValue.Prop[6] +} +``` + +其中: + +- `prop` 索引:`[攻, 防, 特攻, 特防, 速度, 命中]` +- 对应值 `> 0` 代表强化,`< 0` 代表下降,`0` 代表无变化 + +返回值: + +- `true`:继续行动 +- `false`:阻止行动 +- 不返回:默认回退到 `hookaction.hookaction` + +## 4. 脚本示例 + +### 4.1 判断对方是否存在强化(你问的这个) + +```js +function hookAction(hookaction) { + if (!hookaction.hookaction) return false; + + var oppAtk = hookaction.opp_attack; + var oppHasBuff = false; + if (oppAtk && oppAtk.prop) { + for (var i = 0; i < oppAtk.prop.length; i++) { + if (oppAtk.prop[i] > 0) { + oppHasBuff = true; + break; + } + } + } + + if (oppHasBuff) { + // 对方有强化时,放一个针对技能 + useSkill(5001); + return true; + } + + return true; +} +``` + +### 4.2 判断对方是否有异常状态 + +```js +function hookAction(hookaction) { + if (!hookaction.hookaction) return false; + + var oppAtk = hookaction.opp_attack; + var hasStatus = false; + if (oppAtk && oppAtk.status) { + for (var i = 0; i < oppAtk.status.length; i++) { + if (oppAtk.status[i] > 0) { + hasStatus = true; + break; + } + } + } + + if (!hasStatus) { + // 没有异常时尝试上异常 + useSkill(6002); + } + + return true; +} +``` + +## 5. Monaco 类型提示 + +```ts +import * as monaco from "monaco-editor"; + +monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + allowNonTsExtensions: true, + checkJs: true, + target: monaco.languages.typescript.ScriptTarget.ES2020, +}); + +monaco.languages.typescript.javascriptDefaults.addExtraLib( + ` +interface BossHookPetContext { + pet_id: number; + catch_time: number; + hp: number; + max_hp: number; +} + +interface BossHookSkillContext { + skill_id: number; + pp: number; + can_use: boolean; +} + +interface BossHookAttackContext { + skill_id: number; + attack_time: number; + is_critical: number; + lost_hp: number; + gain_hp: number; + remain_hp: number; + max_hp: number; + state: number; + offensive: number; + status: number[]; + prop: number[]; +} + +interface BossHookActionContext { + hookaction: boolean; + round: number; + is_first: boolean; + our: BossHookPetContext | null; + opp: BossHookPetContext | null; + skills: BossHookSkillContext[]; + our_attack: BossHookAttackContext | null; + opp_attack: BossHookAttackContext | null; +} + +declare function hookAction(hookaction: BossHookActionContext): boolean; +declare function HookAction(hookaction: BossHookActionContext): boolean; +declare function hookaction(hookaction: BossHookActionContext): boolean; + +declare function useSkill(skillId: number): void; +declare function switchPet(catchTime: number): void; +`, + "ts:boss-script.d.ts" +); +``` + +## 6. 后端代码 + +- 脚本执行器与函数绑定:`modules/config/model/boss_pet.go` +- AI 出手转发与上下文构建:`logic/service/fight/input/ai.go` + diff --git a/help/report.md b/help/report.md deleted file mode 100644 index b6dd5a49a..000000000 --- a/help/report.md +++ /dev/null @@ -1,99 +0,0 @@ -# 屎山代码分析报告 - -## 总体评估 - -- **质量评分**: 31.03/100 -- **质量等级**: 🌸 偶有异味 - 基本没事,但是有伤风化 -- **分析文件数**: 203 -- **代码总行数**: 20972 - -## 质量指标 - -| 指标 | 得分 | 权重 | 状态 | -|------|------|------|------| -| 状态管理 | 4.84 | 0.15 | ✓✓ | -| 循环复杂度 | 6.28 | 0.25 | ✓✓ | -| 命名规范 | 25.00 | 0.10 | ✓ | -| 错误处理 | 35.00 | 0.15 | ○ | -| 代码结构 | 45.00 | 0.20 | ○ | -| 代码重复度 | 55.00 | 0.15 | • | -| 注释覆盖率 | 55.94 | 0.15 | • | - -## 问题文件 (Top 5) - -### 1. /workspace/blazing/common/utils/sturc/field.go (得分: 53.85) -**问题分类**: 🔄 复杂度问题:10, 📝 注释问题:1, ⚠️ 其他问题:5 - -**主要问题**: -- 函数 Size 的循环复杂度较高 (12),建议简化 -- 函数 packVal 的循环复杂度过高 (23),考虑重构 -- 函数 Pack 的循环复杂度较高 (14),建议简化 -- 函数 unpackVal 的循环复杂度过高 (21),考虑重构 -- 函数 Unpack 的循环复杂度较高 (12),建议简化 -- 函数 'Size' () 较长 (33 行),可考虑重构 -- 函数 'Size' () 复杂度过高 (12),建议简化 -- 函数 'packVal' () 过长 (69 行),建议拆分 -- 函数 'packVal' () 复杂度严重过高 (23),必须简化 -- 函数 'Pack' () 较长 (48 行),可考虑重构 -- 函数 'Pack' () 复杂度过高 (14),建议简化 -- 函数 'unpackVal' () 过长 (57 行),建议拆分 -- 函数 'unpackVal' () 复杂度严重过高 (21),必须简化 -- 函数 'Unpack' () 较长 (33 行),可考虑重构 -- 函数 'Unpack' () 复杂度过高 (12),建议简化 -- 代码注释率极低 (1.38%),几乎没有注释 - -### 2. /workspace/blazing/common/utils/sturc/fields.go (得分: 46.83) -**问题分类**: 🔄 复杂度问题:4, 📝 注释问题:1, ⚠️ 其他问题:2 - -**主要问题**: -- 函数 Pack 的循环复杂度较高 (12),建议简化 -- 函数 Unpack 的循环复杂度过高 (21),考虑重构 -- 函数 'Pack' () 较长 (42 行),可考虑重构 -- 函数 'Pack' () 复杂度过高 (12),建议简化 -- 函数 'Unpack' () 过长 (73 行),建议拆分 -- 函数 'Unpack' () 复杂度严重过高 (21),必须简化 -- 代码注释率极低 (3.91%),几乎没有注释 - -### 3. /workspace/blazing/common/utils/sturc/parse.go (得分: 46.68) -**问题分类**: 🔄 复杂度问题:4, 📝 注释问题:1, ⚠️ 其他问题:3 - -**主要问题**: -- 代码注释率较低 (6.93%),建议增加注释 -- 函数 parseField 的循环复杂度较高 (13),建议简化 -- 函数 parseFieldsLocked 的循环复杂度过高 (18),考虑重构 -- 函数 'parseField' () 过长 (64 行),建议拆分 -- 函数 'parseField' () 复杂度过高 (13),建议简化 -- 函数 'parseFieldsLocked' () 过长 (64 行),建议拆分 -- 函数 'parseFieldsLocked' () 复杂度严重过高 (18),必须简化 -- 函数 'parseFields' () 较长 (31 行),可考虑重构 - -### 4. /workspace/blazing/common/utils/xml/typeinfo.go (得分: 46.13) -**问题分类**: 🔄 复杂度问题:6, ⚠️ 其他问题:3 - -**主要问题**: -- 函数 getTypeInfo 的循环复杂度过高 (18),考虑重构 -- 函数 structFieldInfo 的循环复杂度过高 (33),考虑重构 -- 函数 addFieldInfo 的循环复杂度过高 (20),考虑重构 -- 函数 'getTypeInfo' () 过长 (58 行),建议拆分 -- 函数 'getTypeInfo' () 复杂度严重过高 (18),必须简化 -- 函数 'structFieldInfo' () 极度过长 (114 行),必须拆分 -- 函数 'structFieldInfo' () 复杂度严重过高 (33),必须简化 -- 函数 'addFieldInfo' () 过长 (66 行),建议拆分 -- 函数 'addFieldInfo' () 复杂度严重过高 (20),必须简化 - -### 5. /workspace/blazing/common/utils/go-jsonrpc/auth/handler.go (得分: 45.61) -**问题分类**: 📝 注释问题:1, ⚠️ 其他问题:1 - -**主要问题**: -- 函数 'ServeHTTP' () 较长 (31 行),可考虑重构 -- 代码注释率极低 (0.00%),几乎没有注释 - -## 改进建议 - -### 高优先级 -- 继续保持当前的代码质量标准 - -### 中优先级 -- 可以考虑进一步优化性能和可读性 -- 完善文档和注释,便于团队协作 - diff --git a/logic/controller/Controller.go b/logic/controller/Controller.go index 9703f379c..697686d1f 100644 --- a/logic/controller/Controller.go +++ b/logic/controller/Controller.go @@ -16,8 +16,10 @@ import ( "github.com/gogf/gf/v2/os/glog" "github.com/lunixbochs/struc" + "github.com/panjf2000/gnet/v2" ) +// Maincontroller 是控制器层共享变量。 var Maincontroller = &Controller{} //注入service // Controller 分发cmd逻辑实现 @@ -44,19 +46,14 @@ func ParseCmd[T any](data []byte) T { // Init 初始化控制器,注册所有cmd处理方法 // 参数 isGame: 标识是否为游戏服务器(true)或登录服务器(false) func Init(isGame bool) { - // 获取控制器实例的反射值 controllerValue := reflect.ValueOf(Maincontroller) - - // 获取控制器类型 controllerType := controllerValue.Type() - // 遍历控制器的所有方法 for i := 0; i < controllerType.NumMethod(); i++ { method := controllerType.Method(i) - methodValue := controllerValue.MethodByName(method.Name) + methodValue := controllerValue.Method(i) methodType := methodValue.Type() - // 获取方法第一个参数的类型(请求结构体) if methodType.NumIn() == 0 { continue } @@ -67,43 +64,46 @@ func Init(isGame bool) { continue } reqType := reqArgType.Elem() + binding := getCmdBinding(reqType) - // 解析请求结构体中的cmd标签 - for _, cmd := range getCmd(reqType) { - if cmd == 0 { // 说明不是有效的注册方法 + for _, cmd := range binding.cmds { + if cmd == 0 { glog.Warning(context.Background(), "方法参数必须包含CMD参数", method.Name, "跳过注册") continue } - // 根据服务器类型过滤cmd - // 登录服务器只处理小于1000的cmd + if methodType.NumIn() != 2 { + glog.Warning(context.Background(), "方法参数数量必须为2", method.Name, "跳过注册") + continue + } + if !isGame && cmd > 1000 { continue } - // 游戏服务器只处理大于等于1000的cmd if isGame && cmd < 1000 { continue } - // 注册命令处理函数 if cool.Config.ServerInfo.IsDebug != 0 { fmt.Println("注册方法", cmd, method.Name) } - cmdInfo := cool.Cmd{ - Func: methodValue, - Req: reqType, - - // Res: , // TODO 待实现对不同用户初始化方法以取消全局cmdcache - } - // 预编译创建req实例的函数:返回结构体指针 reqTypeForNew := reqType - cmdInfo.NewReqFunc = func() interface{} { - return reflect.New(reqTypeForNew).Interface() + cmdInfo := cool.Cmd{ + Func: methodValue, + Req: reqType, + HeaderFieldIndex: append([]int(nil), binding.headerFieldIndex...), + UseConn: methodType.In(1) == connType, + NewReqFunc: func() interface{} { + return reflect.New(reqTypeForNew).Interface() + }, + NewReqValue: func() reflect.Value { + return reflect.New(reqTypeForNew) + }, } - if _, exists := cool.CmdCache[cmd]; exists { // 方法已存在 + if _, exists := cool.CmdCache[cmd]; exists { panic(fmt.Sprintf("命令处理方法已存在,跳过注册 %d %s", cmd, method.Name)) } cool.CmdCache[cmd] = cmdInfo @@ -111,12 +111,20 @@ func Init(isGame bool) { } } -var targetType = reflect.TypeOf(common.TomeeHeader{}) -var cmdTypeCache sync.Map +var ( + targetType = reflect.TypeOf(common.TomeeHeader{}) + connType = reflect.TypeOf((*gnet.Conn)(nil)).Elem() + cmdTypeCache sync.Map +) // 默认返回值(无匹配字段/解析失败时) const defaultCmdValue = 0 +type cmdBinding struct { + cmds []uint32 + headerFieldIndex []int +} + func normalizeStructType(typ reflect.Type) reflect.Type { for typ.Kind() == reflect.Ptr { typ = typ.Elem() @@ -124,92 +132,93 @@ func normalizeStructType(typ reflect.Type) reflect.Type { return typ } -// getCmd 从结构体类型中提取绑定的cmd指令(递归查找嵌套结构体,支持值/指针类型的TomeeHeader) -// 参数 typ: 待解析的结构体类型(支持多层指针) -// 返回值: 解析到的cmd切片,无匹配/解析失败时返回[defaultCmdValue] -func getCmd(typ reflect.Type) []uint32 { +// getCmdBinding 从结构体类型中提取绑定的cmd指令和头字段位置。 +func getCmdBinding(typ reflect.Type) cmdBinding { typ = normalizeStructType(typ) if cached, ok := cmdTypeCache.Load(typ); ok { - return cached.([]uint32) + return cached.(cmdBinding) } - // 非结构体类型直接返回默认值 if typ.Kind() != reflect.Struct { - return []uint32{defaultCmdValue} + binding := cmdBinding{cmds: []uint32{defaultCmdValue}} + cmdTypeCache.Store(typ, binding) + return binding } - if cmd, ok := findCmd(typ, make(map[reflect.Type]struct{})); ok { - cmdTypeCache.Store(typ, cmd) - return cmd + if binding, ok := findCmdBinding(typ, make(map[reflect.Type]struct{})); ok { + cmdTypeCache.Store(typ, binding) + return binding } - // 未找到目标字段/所有解析失败,返回默认值 - defaultCmd := []uint32{defaultCmdValue} - cmdTypeCache.Store(typ, defaultCmd) - return defaultCmd + binding := cmdBinding{cmds: []uint32{defaultCmdValue}} + cmdTypeCache.Store(typ, binding) + return binding } -func findCmd(typ reflect.Type, visiting map[reflect.Type]struct{}) ([]uint32, bool) { +func findCmdBinding(typ reflect.Type, visiting map[reflect.Type]struct{}) (cmdBinding, bool) { typ = normalizeStructType(typ) if typ.Kind() != reflect.Struct { - return nil, false + return cmdBinding{}, false } if _, seen := visiting[typ]; seen { - return nil, false + return cmdBinding{}, false } visiting[typ] = struct{}{} defer delete(visiting, typ) - // 遍历结构体字段,查找TomeeHeader字段并解析cmd for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) - // 尝试解析当前字段的cmd标签 cmdSlice, isHeader, err := parseCmdTagWithStructField(field) - if isHeader && err == nil { // 解析成功,直接返回结果 - return cmdSlice, true + if isHeader && err == nil { + return cmdBinding{ + cmds: cmdSlice, + headerFieldIndex: append([]int(nil), field.Index...), + }, true } - // 递归处理嵌套结构体(值/指针类型) nestedTyp := normalizeStructType(field.Type) - if nestedTyp.Kind() == reflect.Struct { - // 递归查找,找到有效cmd则立即返回 - if nestedCmd, ok := findCmd(nestedTyp, visiting); ok { - return nestedCmd, true - } + if nestedTyp.Kind() != reflect.Struct { + continue } + + nestedBinding, ok := findCmdBinding(nestedTyp, visiting) + if !ok { + continue + } + + fieldIndex := make([]int, 0, len(field.Index)+len(nestedBinding.headerFieldIndex)) + fieldIndex = append(fieldIndex, field.Index...) + fieldIndex = append(fieldIndex, nestedBinding.headerFieldIndex...) + nestedBinding.headerFieldIndex = fieldIndex + return nestedBinding, true } - return nil, false + return cmdBinding{}, false } // parseCmdTagWithStructField 校验字段是否为TomeeHeader(值/指针)并解析cmd标签 // 参数 field: 结构体字段元信息 // 返回值: 解析后的cmd切片,是否为目标类型,解析失败错误 func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, bool, error) { - // 判断字段类型是否为 TomeeHeader 或 *TomeeHeader if field.Type != targetType && !(field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType) { return nil, false, nil } - // 提取cmd标签 cmdStr := field.Tag.Get("cmd") if cmdStr == "" { return nil, true, fmt.Errorf("field %s cmd tag is empty", field.Name) } - // 高性能解析标签为uint32切片(替代gconv,减少第三方依赖且可控) result := make([]uint32, 0, strings.Count(cmdStr, "|")+1) remain := cmdStr for idx := 0; ; idx++ { part, next, found := strings.Cut(remain, "|") - // 去除空白字符(兼容标签中意外的空格) s := strings.TrimSpace(part) if s == "" { return nil, true, fmt.Errorf("field %s cmd tag part %d is empty", field.Name, idx) } - // 手动解析uint32,比gconv更可控,避免隐式转换问题 num, err := strconv.ParseUint(s, 10, 32) if err != nil { return nil, true, fmt.Errorf("field %s cmd tag part %d parse error: %v (value: %s)", diff --git a/logic/controller/action_大师杯.go b/logic/controller/action_大师杯.go index eedff6310..18b8aeb31 100644 --- a/logic/controller/action_大师杯.go +++ b/logic/controller/action_大师杯.go @@ -43,6 +43,7 @@ var masterCupRequiredItems = map[uint32][]ItemS{ }, } +// DASHIbei 处理控制器请求。 func (h Controller) DASHIbei(req *C2s_MASTER_REWARDS, c *player.Player) (result *S2C_MASTER_REWARDS, err errorcode.ErrorCode) { _ = req result = &S2C_MASTER_REWARDS{} @@ -52,6 +53,7 @@ func (h Controller) DASHIbei(req *C2s_MASTER_REWARDS, c *player.Player) (result return } +// DASHIbeiR 处理控制器请求。 func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (result *S2C_MASTER_REWARDSR, err errorcode.ErrorCode) { result = &S2C_MASTER_REWARDSR{} @@ -94,6 +96,7 @@ func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (resul return } +// ItemS 定义请求或响应数据结构。 type ItemS struct { ItemId uint32 ItemCnt uint32 @@ -137,6 +140,7 @@ func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, i } } +// C2s_MASTER_REWARDS 定义请求或响应数据结构。 type C2s_MASTER_REWARDS struct { Head common.TomeeHeader `cmd:"2611" struc:"skip"` //玩家登录 } @@ -147,6 +151,7 @@ type S2C_MASTER_REWARDS struct { Reward []uint32 `json:"Reward"` } +// C2s_MASTER_REWARDSR 定义请求或响应数据结构。 type C2s_MASTER_REWARDSR struct { Head common.TomeeHeader `cmd:"2612" struc:"skip"` //玩家登录 ElementType uint32 diff --git a/logic/controller/action_扭蛋.go b/logic/controller/action_扭蛋.go index fed0bfe2a..cb0d9999b 100644 --- a/logic/controller/action_扭蛋.go +++ b/logic/controller/action_扭蛋.go @@ -12,6 +12,7 @@ import ( "github.com/gogf/gf/v2/util/grand" ) +// EggGamePlay 处理控制器请求。 func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (result *S2C_EGG_GAME_PLAY, err errorcode.ErrorCode) { switch data1.EggNum { diff --git a/logic/controller/action_炫彩碎片.go b/logic/controller/action_炫彩碎片.go index a51599a2c..9f2e32c57 100644 --- a/logic/controller/action_炫彩碎片.go +++ b/logic/controller/action_炫彩碎片.go @@ -37,6 +37,7 @@ func Draw15To10WithBitSet() uint32 { return resultBits } +// GET_XUANCAI 处理控制器请求。 func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result *S2C_GET_XUANCAI, err errorcode.ErrorCode) { result = &S2C_GET_XUANCAI{} selectedCount := 0 // 已选中的数量 @@ -74,6 +75,7 @@ func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result } +// C2s_GET_XUANCAI 定义请求或响应数据结构。 type C2s_GET_XUANCAI struct { Head common.TomeeHeader `cmd:"60001" struc:"skip"` //玩家登录 } diff --git a/logic/controller/action_超时空隧道.go b/logic/controller/action_超时空隧道.go index 8e8620bf4..2e64fe33d 100644 --- a/logic/controller/action_超时空隧道.go +++ b/logic/controller/action_超时空隧道.go @@ -30,6 +30,7 @@ func (h Controller) TimeMap(data *C2s_SP, c *player.Player) (result *S2C_SP, err } +// C2s_SP 定义请求或响应数据结构。 type C2s_SP struct { Head common.TomeeHeader `cmd:"60002" struc:"skip"` //超时空地图 } @@ -40,6 +41,7 @@ type S2C_SP struct { MapList []ServerInfo } +// ServerInfo 定义请求或响应数据结构。 type ServerInfo struct { ID uint32 //地图ID PetLen uint32 `struc:"sizeof=Pet"` diff --git a/logic/controller/activce_雷伊特训.go b/logic/controller/activce_雷伊特训.go index d9dce9955..a8a0d7d28 100644 --- a/logic/controller/activce_雷伊特训.go +++ b/logic/controller/activce_雷伊特训.go @@ -7,6 +7,7 @@ import ( "blazing/logic/service/player" ) +// GetLeiyiTrainStatus 处理控制器请求。 func (h Controller) GetLeiyiTrainStatus(data *C2s_LEIYI_TRAIN_GET_STATUS, c *player.Player) (result *S2C_LEIYI_TRAIN_GET_STATUS, err errorcode.ErrorCode) { result = &S2C_LEIYI_TRAIN_GET_STATUS{} @@ -19,6 +20,7 @@ func (h Controller) GetLeiyiTrainStatus(data *C2s_LEIYI_TRAIN_GET_STATUS, c *pla } +// C2s_LEIYI_TRAIN_GET_STATUS 定义请求或响应数据结构。 type C2s_LEIYI_TRAIN_GET_STATUS struct { Head common.TomeeHeader `cmd:"2393" struc:"skip"` //玩家登录 } @@ -28,6 +30,7 @@ type S2C_LEIYI_TRAIN_GET_STATUS struct { Status [10]S2C_LEIYI_TRAIN_GET_STATUS_info `json:"status"` } +// S2C_LEIYI_TRAIN_GET_STATUS_info 定义请求或响应数据结构。 type S2C_LEIYI_TRAIN_GET_STATUS_info struct { // Today uint32 // 今日训练HP次数 Current uint32 // 当前训练HP次数 diff --git a/logic/controller/active_寒流枪.go b/logic/controller/active_寒流枪.go index f556e3ec1..ca5f1665b 100644 --- a/logic/controller/active_寒流枪.go +++ b/logic/controller/active_寒流枪.go @@ -35,6 +35,7 @@ func (h Controller) HanLiuQiang(data *C2S_2608, c *player.Player) (result *fight return result, -1 } +// C2S_2608 定义请求或响应数据结构。 type C2S_2608 struct { Head common.TomeeHeader `cmd:"2608" struc:"skip"` } diff --git a/logic/controller/fight_base.go b/logic/controller/fight_base.go index 30ef8bd7b..b9278bbaf 100644 --- a/logic/controller/fight_base.go +++ b/logic/controller/fight_base.go @@ -2,7 +2,6 @@ package controller import ( "blazing/common/socket/errorcode" - "blazing/modules/player/model" "blazing/logic/service/fight" "blazing/logic/service/fight/info" @@ -31,7 +30,7 @@ func (h Controller) UseSkill(data *UseSkillInInfo, c *player.Player) (result *fi if err := h.checkFightStatus(c); err != 0 { return nil, err } - go c.FightC.UseSkill(c, data.SkillId) + h.dispatchFightActionEnvelope(c, buildLegacyUseSkillEnvelope(data)) return nil, 0 } @@ -41,31 +40,7 @@ func (h Controller) UseSkillAt(data *UseSkillAtInboundInfo, c *player.Player) (r if err := h.checkFightStatus(c); err != 0 { return nil, err } - actorIndex := int(data.ActorIndex) - targetIndex := int(data.TargetIndex) - targetRelation := data.TargetRelation - - // 前端未显式给 relation 时,按 AtkType 兜底:3=自己,1=己方,其他按对方。 - if targetRelation > fight.SkillTargetAlly { - switch data.AtkType { - case 3: - targetRelation = fight.SkillTargetSelf - case 1: - targetRelation = fight.SkillTargetAlly - default: - targetRelation = fight.SkillTargetOpponent - } - } - - switch targetRelation { - case fight.SkillTargetSelf: - targetIndex = actorIndex - go c.FightC.UseSkillAt(c, data.SkillId, actorIndex, fight.EncodeTargetIndex(targetIndex, false)) - case fight.SkillTargetAlly: - go c.FightC.UseSkillAt(c, data.SkillId, actorIndex, fight.EncodeTargetIndex(targetIndex, false)) - default: - go c.FightC.UseSkillAt(c, data.SkillId, actorIndex, fight.EncodeTargetIndex(targetIndex, true)) - } + h.dispatchFightActionEnvelope(c, buildIndexedUseSkillEnvelope(data)) return nil, 0 } @@ -74,8 +49,7 @@ func (h Controller) Escape(data *EscapeFightInboundInfo, c *player.Player) (resu if err := h.checkFightStatus(c); err != 0 { return nil, err } - - go c.FightC.Over(c, model.BattleOverReason.PlayerEscape) + h.dispatchFightActionEnvelope(c, buildLegacyEscapeEnvelope()) return nil, 0 } @@ -84,7 +58,7 @@ func (h Controller) ChangePet(data *ChangePetInboundInfo, c *player.Player) (res if err := h.checkFightStatus(c); err != 0 { return nil, err } - go c.FightC.ChangePet(c, data.CatchTime) + h.dispatchFightActionEnvelope(c, buildLegacyChangeEnvelope(data)) return nil, -1 } @@ -123,7 +97,7 @@ func (h Controller) UsePetItemInboundInfo(data *UsePetItemInboundInfo, c *player } } - go c.FightC.UseItem(c, data.CatchTime, data.ItemId) + h.dispatchFightActionEnvelope(c, buildLegacyUseItemEnvelope(data)) return nil, -1 } @@ -132,6 +106,6 @@ func (h Controller) FightChat(data *ChatInfo, c *player.Player) (result *fight.N if err := h.checkFightStatus(c); err != 0 { return nil, err } - go c.FightC.Chat(c, data.Message) + h.dispatchFightActionEnvelope(c, buildChatEnvelope(data)) return nil, -1 } diff --git a/logic/controller/fight_boss野怪和地图怪.go b/logic/controller/fight_boss野怪和地图怪.go index d915a6fef..ec519c69c 100644 --- a/logic/controller/fight_boss野怪和地图怪.go +++ b/logic/controller/fight_boss野怪和地图怪.go @@ -27,7 +27,10 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe return nil, err } - mapNode := service.NewMapNodeService().GetDataNode(p.Info.MapID, req.BossId) + mapNode := p.GetSpace().GetMatchedMapNode(req.BossId) + if mapNode == nil { + return nil, errorcode.ErrorCodes.ErrPokemonNotExists + } bossConfigs, err := loadMapBossConfigs(mapNode) if err != 0 { return nil, err @@ -43,7 +46,8 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe ai := player.NewAI_player(monsterInfo) ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID) - ai.Prop[0] = 2 + ai.BossScript = bossConfigs[0].Script + ai.AddBattleProp(0, 2) var fightC *fight.FightC fightC, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) { diff --git a/logic/controller/fight_pvp_king.go b/logic/controller/fight_pvp_king.go index e82d86161..64522bbbf 100644 --- a/logic/controller/fight_pvp_king.go +++ b/logic/controller/fight_pvp_king.go @@ -70,9 +70,12 @@ func (h Controller) PetMelee(data *StartPetWarInboundInfo, c *player.Player) (re return } + +// PetKing 处理控制器请求。 func (h Controller) PetKing(data *PetKingJoinInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { c.Fightinfo.Status = info.BattleMode.PET_TOPLEVEL + // ElementTypeNumbers 是控制器层共享变量。 var ElementTypeNumbers = []int{1, 2, 3, 5, 11, 4, 6, 7, 9} switch data.Type { diff --git a/logic/controller/fight_pvp_withplayer.go b/logic/controller/fight_pvp_withplayer.go index e3a911762..414c937a5 100644 --- a/logic/controller/fight_pvp_withplayer.go +++ b/logic/controller/fight_pvp_withplayer.go @@ -55,7 +55,7 @@ func (h Controller) OnPlayerHandleFightInvite(data *HandleFightInviteInboundInfo return } - _, err = fight.NewFight(v, c, v.GetInfo().PetList, c.GetInfo().PetList, func(foi model.FightOverInfo) { + _, err = fight.NewFight(v, c, v.GetPetInfo(100), c.GetPetInfo(100), func(foi model.FightOverInfo) { //println("好友对战测试", foi.Reason) diff --git a/logic/controller/fight_unified.go b/logic/controller/fight_unified.go new file mode 100644 index 000000000..c9f9cc557 --- /dev/null +++ b/logic/controller/fight_unified.go @@ -0,0 +1,79 @@ +package controller + +import ( + "blazing/modules/player/model" + + "blazing/logic/service/fight" + "blazing/logic/service/player" +) + +// dispatchFightActionEnvelope 把控制器层收到的统一动作结构分发回现有 FightI 接口。 +func (h Controller) dispatchFightActionEnvelope(c *player.Player, envelope fight.FightActionEnvelope) { + if c == nil || c.FightC == nil { + return + } + + switch envelope.ActionType { + case fight.FightActionTypeSkill: + go c.FightC.UseSkillAt(c, envelope.SkillID, envelope.ActorIndex, envelope.EncodedTargetIndex()) + case fight.FightActionTypeItem: + go c.FightC.UseItemAt(c, envelope.CatchTime, envelope.ItemID, envelope.ActorIndex, envelope.EncodedTargetIndex()) + case fight.FightActionTypeChange: + go c.FightC.ChangePetAt(c, envelope.CatchTime, envelope.ActorIndex) + case fight.FightActionTypeEscape: + go c.FightC.Over(c, model.BattleOverReason.PlayerEscape) + case fight.FightActionTypeChat: + go c.FightC.Chat(c, envelope.Chat) + } +} + +// buildLegacyUseSkillEnvelope 把旧 2405 技能包映射成统一动作结构。 +func buildLegacyUseSkillEnvelope(data *UseSkillInInfo) fight.FightActionEnvelope { + if data == nil { + return fight.NewSkillActionEnvelope(0, 0, 0, fight.SkillTargetOpponent, 0) + } + return fight.NewSkillActionEnvelope(data.SkillId, 0, 0, fight.SkillTargetOpponent, 0) +} + +// buildIndexedUseSkillEnvelope 把 7505 多战位技能包映射成统一动作结构。 +func buildIndexedUseSkillEnvelope(data *UseSkillAtInboundInfo) fight.FightActionEnvelope { + if data == nil { + return fight.NewSkillActionEnvelope(0, 0, 0, fight.SkillTargetOpponent, 0) + } + return fight.NewSkillActionEnvelope( + data.SkillId, + int(data.ActorIndex), + int(data.TargetIndex), + data.TargetRelation, + data.AtkType, + ) +} + +// buildLegacyUseItemEnvelope 把旧 2406 道具包映射成统一动作结构。 +func buildLegacyUseItemEnvelope(data *UsePetItemInboundInfo) fight.FightActionEnvelope { + if data == nil { + return fight.NewItemActionEnvelope(0, 0, 0, 0, fight.SkillTargetOpponent) + } + return fight.NewItemActionEnvelope(data.CatchTime, data.ItemId, 0, 0, fight.SkillTargetOpponent) +} + +// buildLegacyChangeEnvelope 把旧 2407 切宠包映射成统一动作结构。 +func buildLegacyChangeEnvelope(data *ChangePetInboundInfo) fight.FightActionEnvelope { + if data == nil { + return fight.NewChangeActionEnvelope(0, 0) + } + return fight.NewChangeActionEnvelope(data.CatchTime, 0) +} + +// buildLegacyEscapeEnvelope 构造旧 2410 逃跑包对应的统一动作结构。 +func buildLegacyEscapeEnvelope() fight.FightActionEnvelope { + return fight.NewEscapeActionEnvelope() +} + +// buildChatEnvelope 把战斗聊天包映射成统一动作结构。 +func buildChatEnvelope(data *ChatInfo) fight.FightActionEnvelope { + if data == nil { + return fight.NewChatActionEnvelope("") + } + return fight.NewChatActionEnvelope(data.Message) +} diff --git a/logic/controller/fight_塔.go b/logic/controller/fight_塔.go index e8369ff21..3aa007235 100644 --- a/logic/controller/fight_塔.go +++ b/logic/controller/fight_塔.go @@ -82,6 +82,7 @@ func (h Controller) FreshChoiceFightLevel(data *C2S_FRESH_CHOICE_FIGHT_LEVEL, c return result, 0 } +// FreshLeaveFightLevel 处理控制器请求。 func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { _ = data defer c.GetSpace().EnterMap(c) @@ -92,6 +93,7 @@ func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *playe return result, 0 } +// PetTawor 处理控制器请求。 func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) { if err = c.CanFight(); err != 0 { return nil, err @@ -110,7 +112,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 +121,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 +198,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 +237,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/controller/fight_巅峰.go b/logic/controller/fight_巅峰.go index 6cbd61c0f..a0ef65ff2 100644 --- a/logic/controller/fight_巅峰.go +++ b/logic/controller/fight_巅峰.go @@ -15,6 +15,7 @@ type PetTOPLEVELnboundInfo struct { } +// JoINtop 处理控制器请求。 func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { err = pvp.JoinPeakQueue(c, data.Mode) if err != 0 { @@ -23,11 +24,13 @@ func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (resu return nil, -1 } +// CancelPeakQueue 处理控制器请求。 func (h Controller) CancelPeakQueue(data *PeakQueueCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { pvp.CancelPeakQueue(c) return nil, -1 } +// SubmitPeakBanPick 处理控制器请求。 func (h Controller) SubmitPeakBanPick(data *PeakBanPickSubmitInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { err = pvp.SubmitBanPick(c, data.SelectedCatchTimes, data.BanCatchTimes) if err != 0 { diff --git a/logic/controller/fight_擂台.go b/logic/controller/fight_擂台.go index f79d32077..5715ea736 100644 --- a/logic/controller/fight_擂台.go +++ b/logic/controller/fight_擂台.go @@ -12,6 +12,7 @@ import ( "blazing/logic/service/space" ) +// ARENA_SET_OWENR 定义请求或响应数据结构。 type ARENA_SET_OWENR struct { Head common.TomeeHeader `cmd:"2417" struc:"skip"` } @@ -35,6 +36,7 @@ func (h Controller) ArenaSetOwner(data *ARENA_SET_OWENR, c *player.Player) (resu return nil, errorcode.ErrorCodes.ErrChampionExists } +// ARENA_FIGHT_OWENR 定义请求或响应数据结构。 type ARENA_FIGHT_OWENR struct { Head common.TomeeHeader `cmd:"2418" struc:"skip"` } diff --git a/logic/controller/inbound_fight.go b/logic/controller/inbound_fight.go index be2695e51..f36472cbe 100644 --- a/logic/controller/inbound_fight.go +++ b/logic/controller/inbound_fight.go @@ -2,56 +2,66 @@ package controller import "blazing/logic/service/common" +// FightNpcMonsterInboundInfo 定义请求或响应数据结构。 type FightNpcMonsterInboundInfo struct { Head common.TomeeHeader `cmd:"2408" struc:"skip"` Number uint32 `fieldDesc:"地图刷新怪物结构体对应的序号 1 - 9 的位置序号" ` } +// ChallengeBossInboundInfo 定义请求或响应数据结构。 type ChallengeBossInboundInfo struct { Head common.TomeeHeader `cmd:"2411" struc:"skip"` BossId uint32 `json:"bossId"` } +// ReadyToFightInboundInfo 定义请求或响应数据结构。 type ReadyToFightInboundInfo struct { Head common.TomeeHeader `cmd:"2404" struc:"skip"` } +// EscapeFightInboundInfo 定义请求或响应数据结构。 type EscapeFightInboundInfo struct { Head common.TomeeHeader `cmd:"2410" struc:"skip"` } +// StartPetWarInboundInfo 定义请求或响应数据结构。 type StartPetWarInboundInfo struct { Head common.TomeeHeader `cmd:"2431" struc:"skip"` } +// StartTwarInboundInfo 定义请求或响应数据结构。 type StartTwarInboundInfo struct { Head common.TomeeHeader `cmd:"2429|2415|2425" struc:"skip"` } - - +// ARENA_GET_INFO 定义请求或响应数据结构。 type ARENA_GET_INFO struct { Head common.TomeeHeader `cmd:"2419" struc:"skip"` } +// ARENA_UPFIGHT 定义请求或响应数据结构。 type ARENA_UPFIGHT struct { Head common.TomeeHeader `cmd:"2420" struc:"skip"` } +// ARENA_OWENR_ACCE 定义请求或响应数据结构。 type ARENA_OWENR_ACCE struct { Head common.TomeeHeader `cmd:"2422" struc:"skip"` } +// PetKingJoinInboundInfo 定义请求或响应数据结构。 type PetKingJoinInboundInfo struct { Head common.TomeeHeader `cmd:"2413" struc:"skip"` Type uint32 FightType uint32 } +// PeakQueueCancelInboundInfo 定义请求或响应数据结构。 type PeakQueueCancelInboundInfo struct { Head common.TomeeHeader `cmd:"2459" struc:"skip"` } +// PeakBanPickSubmitInboundInfo 定义请求或响应数据结构。 type PeakBanPickSubmitInboundInfo struct { Head common.TomeeHeader `cmd:"2460" struc:"skip"` @@ -62,6 +72,7 @@ type PeakBanPickSubmitInboundInfo struct { BanCatchTimes []uint32 `json:"banCatchTimes"` } +// HandleFightInviteInboundInfo 定义请求或响应数据结构。 type HandleFightInviteInboundInfo struct { Head common.TomeeHeader `cmd:"2403" struc:"skip"` UserID uint32 `json:"userId" codec:"userId,uint"` @@ -69,21 +80,25 @@ type HandleFightInviteInboundInfo struct { Mode uint32 `json:"mode" codec:"mode,uint"` } +// InviteToFightInboundInfo 定义请求或响应数据结构。 type InviteToFightInboundInfo struct { Head common.TomeeHeader `cmd:"2401" struc:"skip"` UserID uint32 Mode uint32 } +// InviteFightCancelInboundInfo 定义请求或响应数据结构。 type InviteFightCancelInboundInfo struct { Head common.TomeeHeader `cmd:"2402" struc:"skip"` } +// UseSkillInInfo 定义请求或响应数据结构。 type UseSkillInInfo struct { Head common.TomeeHeader `cmd:"2405" struc:"skip"` SkillId uint32 } +// UseSkillAtInboundInfo 定义请求或响应数据结构。 type UseSkillAtInboundInfo struct { Head common.TomeeHeader `cmd:"7505" struc:"skip"` SkillId uint32 `json:"skillId"` @@ -93,21 +108,25 @@ type UseSkillAtInboundInfo struct { AtkType uint8 `json:"atkType"` } +// ChangePetInboundInfo 定义请求或响应数据结构。 type ChangePetInboundInfo struct { Head common.TomeeHeader `cmd:"2407" struc:"skip"` CatchTime uint32 `json:"catchTime"` } +// CatchMonsterInboundInfo 定义请求或响应数据结构。 type CatchMonsterInboundInfo struct { Head common.TomeeHeader `cmd:"2409" struc:"skip"` CapsuleId uint32 `json:"capsuleId" fieldDescription:"胶囊id" uint:"true"` } +// LoadPercentInboundInfo 定义请求或响应数据结构。 type LoadPercentInboundInfo struct { Head common.TomeeHeader `cmd:"2441" struc:"skip"` Percent uint32 `fieldDescription:"加载百分比"` } +// UsePetItemInboundInfo 定义请求或响应数据结构。 type UsePetItemInboundInfo struct { Head common.TomeeHeader `cmd:"2406" struc:"skip"` CatchTime uint32 `description:"精灵捕获时间" codec:"catchTime"` @@ -115,6 +134,7 @@ type UsePetItemInboundInfo struct { Reversed1 uint32 `description:"填充字段 0" codec:"reversed1"` } +// ChatInfo 定义请求或响应数据结构。 type ChatInfo struct { Head common.TomeeHeader `cmd:"50002" struc:"skip"` Reserve uint32 `json:"reserve" fieldDescription:"填充 默认值为0" uint:"true"` @@ -122,16 +142,19 @@ type ChatInfo struct { Message string `json:"message" fieldDescription:"消息内容, 结束符为utf-8的数字0"` } +// C2S_FRESH_CHOICE_FIGHT_LEVEL 定义请求或响应数据结构。 type C2S_FRESH_CHOICE_FIGHT_LEVEL struct { Head common.TomeeHeader `cmd:"2428|2414" struc:"skip"` Level uint `json:"level"` } +// C2S_OPEN_DARKPORTAL 定义请求或响应数据结构。 type C2S_OPEN_DARKPORTAL struct { Head common.TomeeHeader `cmd:"2424" struc:"skip"` Level uint32 `json:"level"` } +// FRESH_LEAVE_FIGHT_LEVEL 定义请求或响应数据结构。 type FRESH_LEAVE_FIGHT_LEVEL struct { Head common.TomeeHeader `cmd:"2430|2416|2426" struc:"skip"` } diff --git a/logic/controller/inbound_friend_task.go b/logic/controller/inbound_friend_task.go index 6bfdb4f9a..4ef295ee4 100644 --- a/logic/controller/inbound_friend_task.go +++ b/logic/controller/inbound_friend_task.go @@ -2,50 +2,59 @@ package controller import "blazing/logic/service/common" +// SeeOnlineInboundInfo 定义请求或响应数据结构。 type SeeOnlineInboundInfo struct { Head common.TomeeHeader `cmd:"2157" struc:"skip"` UserIdsLen uint32 `json:"userIdsLen" struc:"sizeof=UserIds"` UserIds []uint32 `json:"userIds" ` } +// FriendAddInboundInfo 定义请求或响应数据结构。 type FriendAddInboundInfo struct { Head common.TomeeHeader `cmd:"2151" struc:"skip"` UserID uint32 `json:"userID"` } +// FriendAnswerInboundInfo 定义请求或响应数据结构。 type FriendAnswerInboundInfo struct { Head common.TomeeHeader `cmd:"2152" struc:"skip"` UserID uint32 `json:"userID"` Flag uint32 `json:"flag"` } +// FriendRemoveInboundInfo 定义请求或响应数据结构。 type FriendRemoveInboundInfo struct { Head common.TomeeHeader `cmd:"2153" struc:"skip"` UserID uint32 `json:"userID"` } +// AcceptTaskInboundInfo 定义请求或响应数据结构。 type AcceptTaskInboundInfo struct { Head common.TomeeHeader `cmd:"2201|2231" struc:"skip"` TaskId uint32 `json:"taskId" description:"任务ID"` } +// AddTaskBufInboundInfo 定义请求或响应数据结构。 type AddTaskBufInboundInfo struct { Head common.TomeeHeader `cmd:"2204|2235" struc:"skip"` TaskId uint32 `json:"taskId" description:"任务ID"` TaskList []uint32 `struc:"[20]byte"` } +// CompleteTaskInboundInfo 定义请求或响应数据结构。 type CompleteTaskInboundInfo struct { Head common.TomeeHeader `cmd:"2202|2233" struc:"skip"` TaskId uint32 `json:"taskId" description:"任务ID"` OutState uint32 `json:"outState" ` } +// GetTaskBufInboundInfo 定义请求或响应数据结构。 type GetTaskBufInboundInfo struct { Head common.TomeeHeader `cmd:"2203|2234" struc:"skip"` TaskId uint32 `json:"taskId" description:"任务ID"` } +// DeleteTaskInboundInfo 定义请求或响应数据结构。 type DeleteTaskInboundInfo struct { Head common.TomeeHeader `cmd:"2205|2232" struc:"skip"` TaskId uint32 `json:"taskId" description:"任务ID"` diff --git a/logic/controller/inbound_item.go b/logic/controller/inbound_item.go index a41e10a0e..05e3a230a 100644 --- a/logic/controller/inbound_item.go +++ b/logic/controller/inbound_item.go @@ -2,18 +2,21 @@ package controller import "blazing/logic/service/common" +// BuyInboundInfo 定义请求或响应数据结构。 type BuyInboundInfo struct { Head common.TomeeHeader `cmd:"2601" struc:"skip"` ItemId int64 `struc:"uint32"` Count int64 `struc:"uint32"` } +// BuyMultiInboundInfo 定义请求或响应数据结构。 type BuyMultiInboundInfo struct { Head common.TomeeHeader `cmd:"2606" struc:"skip"` ItemListLen uint32 `struc:"sizeof=ItemIds"` ItemIds []uint32 `json:"itemIds" description:"购买的物品ID列表"` } +// C2S_GOLD_BUY_PRODUCT 定义请求或响应数据结构。 type C2S_GOLD_BUY_PRODUCT struct { Head common.TomeeHeader `cmd:"1104" struc:"skip"` Type uint32 `json:"type"` @@ -21,6 +24,7 @@ type C2S_GOLD_BUY_PRODUCT struct { Count int64 `struc:"uint32"` } +// ItemListInboundInfo 定义请求或响应数据结构。 type ItemListInboundInfo struct { Head common.TomeeHeader `cmd:"2605|4475" struc:"skip"` Param1 uint32 @@ -28,36 +32,43 @@ type ItemListInboundInfo struct { Param3 uint32 } +// GoldOnlineRemainInboundInfo 定义请求或响应数据结构。 type GoldOnlineRemainInboundInfo struct { Head common.TomeeHeader `cmd:"1105|1106" struc:"skip"` } +// ExpTotalRemainInboundInfo 定义请求或响应数据结构。 type ExpTotalRemainInboundInfo struct { Head common.TomeeHeader `cmd:"2319" struc:"skip"` } +// ChangePlayerClothInboundInfo 定义请求或响应数据结构。 type ChangePlayerClothInboundInfo struct { Head common.TomeeHeader `cmd:"2604" struc:"skip"` ClothesLen uint32 `struc:"sizeof=ClothList" fieldDesc:"穿戴装备的信息" json:"clothes_len"` ClothList []uint32 `description:"玩家装备列表" codec:"list"` } +// TalkCountInboundInfo 定义请求或响应数据结构。 type TalkCountInboundInfo struct { Head common.TomeeHeader `cmd:"2701" struc:"skip"` ID uint32 `description:"奖品的Type, 即ID" codec:"uint"` } +// TalkCateInboundInfo 定义请求或响应数据结构。 type TalkCateInboundInfo struct { Head common.TomeeHeader `cmd:"2702" struc:"skip"` ID uint32 `description:"奖品的Type, 即ID" codec:"uint"` } +// C2S_USE_PET_ITEM_OUT_OF_FIGHT 定义请求或响应数据结构。 type C2S_USE_PET_ITEM_OUT_OF_FIGHT struct { Head common.TomeeHeader `cmd:"2326" struc:"skip"` CatchTime uint32 `json:"catch_time"` ItemID int32 `struc:"uint32"` } +// C2S_PET_RESET_NATURE 定义请求或响应数据结构。 type C2S_PET_RESET_NATURE struct { Head common.TomeeHeader `cmd:"2343" struc:"skip"` CatchTime uint32 @@ -65,22 +76,26 @@ type C2S_PET_RESET_NATURE struct { ItemId uint32 } +// C2S_ITEM_SALE 定义请求或响应数据结构。 type C2S_ITEM_SALE struct { Head common.TomeeHeader `cmd:"2602" struc:"skip"` ItemId uint32 Amount uint32 } +// C2S_USE_SPEEDUP_ITEM 定义请求或响应数据结构。 type C2S_USE_SPEEDUP_ITEM struct { Head common.TomeeHeader `cmd:"2327" struc:"skip"` ItemID uint32 } +// C2S_USE_ENERGY_XISHOU 定义请求或响应数据结构。 type C2S_USE_ENERGY_XISHOU struct { Head common.TomeeHeader `cmd:"2331" struc:"skip"` ItemID uint32 } +// C2S_USE_AUTO_FIGHT_ITEM 定义请求或响应数据结构。 type C2S_USE_AUTO_FIGHT_ITEM struct { Head common.TomeeHeader `cmd:"2329" struc:"skip"` ItemID uint32 diff --git a/logic/controller/inbound_map_room_nono.go b/logic/controller/inbound_map_room_nono.go index 011320c09..a4b8573cb 100644 --- a/logic/controller/inbound_map_room_nono.go +++ b/logic/controller/inbound_map_room_nono.go @@ -5,6 +5,7 @@ import ( "blazing/modules/player/model" ) +// EnterMapInboundInfo 定义请求或响应数据结构。 type EnterMapInboundInfo struct { Head common.TomeeHeader `cmd:"2001" struc:"skip"` MapType uint32 @@ -12,22 +13,27 @@ type EnterMapInboundInfo struct { Point model.Pos `fieldDesc:"直接给坐标x,y"` } +// GetMapHotInboundInfo 定义请求或响应数据结构。 type GetMapHotInboundInfo struct { Head common.TomeeHeader `cmd:"1004" struc:"skip"` } +// LeaveMapInboundInfo 定义请求或响应数据结构。 type LeaveMapInboundInfo struct { Head common.TomeeHeader `cmd:"2002" struc:"skip"` } +// ListMapPlayerInboundInfo 定义请求或响应数据结构。 type ListMapPlayerInboundInfo struct { Head common.TomeeHeader `cmd:"2003" struc:"skip"` } +// AttackBossInboundInfo 定义请求或响应数据结构。 type AttackBossInboundInfo struct { Head common.TomeeHeader `cmd:"2412" struc:"skip"` } +// WalkInInfo 定义请求或响应数据结构。 type WalkInInfo struct { Head common.TomeeHeader `cmd:"2101" struc:"skip"` Flag uint32 @@ -36,20 +42,24 @@ type WalkInInfo struct { Path string } +// FitmentUseringInboundInfo 定义请求或响应数据结构。 type FitmentUseringInboundInfo struct { Head common.TomeeHeader `cmd:"10006" struc:"skip"` TargetUserID uint32 `json:"targetUserId"` } +// PetRoomListInboundInfo 定义请求或响应数据结构。 type PetRoomListInboundInfo struct { Head common.TomeeHeader `cmd:"2324" struc:"skip"` TargetUserID uint32 `json:"targetUserId"` } +// FitmentAllInboundEmpty 定义请求或响应数据结构。 type FitmentAllInboundEmpty struct { Head common.TomeeHeader `cmd:"10007" struc:"skip"` } +// SET_FITMENT 定义请求或响应数据结构。 type SET_FITMENT struct { Head common.TomeeHeader `cmd:"10008" struc:"skip"` RoomID uint32 `json:"roomID"` @@ -57,44 +67,52 @@ type SET_FITMENT struct { Fitments []model.FitmentShowInfo `json:"usedList"` } +// C2S_PetShowList 定义请求或响应数据结构。 type C2S_PetShowList struct { CatchTime uint32 `json:"catchTime"` PetID uint32 `json:"petID"` } +// C2S_PET_ROOM_SHOW 定义请求或响应数据结构。 type C2S_PET_ROOM_SHOW struct { Head common.TomeeHeader `cmd:"2323" struc:"skip"` PetShowInfoLen uint32 `json:"PetShowInfoLen" struc:"sizeof=PetShowList"` PetShowList []C2S_PetShowList `json:"PetShowList"` } +// C2S_RoomPetInfo 定义请求或响应数据结构。 type C2S_RoomPetInfo struct { Head common.TomeeHeader `cmd:"2325" struc:"skip"` UserID uint32 `json:"userID"` CatchTime uint32 `json:"catchTime"` } +// C2S_BUY_FITMENT 定义请求或响应数据结构。 type C2S_BUY_FITMENT struct { Head common.TomeeHeader `cmd:"10004" struc:"skip"` ID uint32 `json:"id"` Count uint32 `json:"count"` } +// NonoInboundInfo 定义请求或响应数据结构。 type NonoInboundInfo struct { Head common.TomeeHeader `cmd:"9003" struc:"skip"` UserID uint32 } +// NonoFollowOrHomeInInfo 定义请求或响应数据结构。 type NonoFollowOrHomeInInfo struct { Head common.TomeeHeader `cmd:"9019" struc:"skip"` Flag uint32 `fieldDescription:"1为跟随 0为收回 且如果为收回 那么后续结构不需要发送" uint:"true"` } +// SwitchFlyingInboundInfo 定义请求或响应数据结构。 type SwitchFlyingInboundInfo struct { Head common.TomeeHeader `cmd:"2112" struc:"skip"` Type uint32 `description:"开关, 0为取消飞行模式, 大于0为开启飞行模式" codec:"auto" uint:"true"` } +// PetCureInboundInfo 定义请求或响应数据结构。 type PetCureInboundInfo struct { Head common.TomeeHeader `cmd:"2306" struc:"skip"` } diff --git a/logic/controller/inbound_pet.go b/logic/controller/inbound_pet.go index d1e48f593..240c5a2b1 100644 --- a/logic/controller/inbound_pet.go +++ b/logic/controller/inbound_pet.go @@ -2,15 +2,18 @@ package controller import "blazing/logic/service/common" +// GetPetInfoInboundInfo 定义请求或响应数据结构。 type GetPetInfoInboundInfo struct { Head common.TomeeHeader `cmd:"2301" struc:"skip"` CatchTime uint32 } +// GetUserBagPetInfoInboundEmpty 定义请求或响应数据结构。 type GetUserBagPetInfoInboundEmpty struct { Head common.TomeeHeader `cmd:"4483" struc:"skip"` } +// SavePetBagOrderInboundInfo 定义请求或响应数据结构。 type SavePetBagOrderInboundInfo struct { Head common.TomeeHeader `cmd:"4484" struc:"skip"` @@ -20,51 +23,60 @@ type SavePetBagOrderInboundInfo struct { BackupPetList []uint32 } +// PetReleaseInboundInfo 定义请求或响应数据结构。 type PetReleaseInboundInfo struct { Head common.TomeeHeader `cmd:"2304" struc:"skip"` CatchTime uint32 Flag uint32 `json:"flag" fieldDescription:"0为放入仓库,1为放入背包" autoCodec:"true" uint:"true"` } +// PetShowInboundInfo 定义请求或响应数据结构。 type PetShowInboundInfo struct { Head common.TomeeHeader `cmd:"2305" struc:"skip"` CatchTime uint32 `codec:"catchTime" inboundMessageType:"Pet_Show"` Flag uint32 `codec:"flag"` } +// PetOneCureInboundInfo 定义请求或响应数据结构。 type PetOneCureInboundInfo struct { Head common.TomeeHeader `cmd:"2310" struc:"skip"` CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"` } +// PET_ROWEI 定义请求或响应数据结构。 type PET_ROWEI struct { Head common.TomeeHeader `cmd:"2321" struc:"skip"` ID uint32 CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"` } +// PET_RETRIEVE 定义请求或响应数据结构。 type PET_RETRIEVE struct { Head common.TomeeHeader `cmd:"2322" struc:"skip"` CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"` } +// PetDefaultInboundInfo 定义请求或响应数据结构。 type PetDefaultInboundInfo struct { Head common.TomeeHeader `cmd:"2308" struc:"skip"` CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true" autoCodec:"true" inboundMessageType:"Pet_Default"` } +// PetSetExpInboundInfo 定义请求或响应数据结构。 type PetSetExpInboundInfo struct { Head common.TomeeHeader `cmd:"2318" struc:"skip"` CatchTime uint32 `fieldDescription:"精灵获取时间" uint:"true" autoCodec:"true"` Exp int64 `struc:"uint32"` } +// PetBargeListInboundInfo 定义请求或响应数据结构。 type PetBargeListInboundInfo struct { Head common.TomeeHeader `cmd:"2309" struc:"skip"` StartPetId uint32 `description:"开始精灵id" codec:"startPetId"` EndPetId uint32 `description:"结束精灵id" codec:"endPetId"` } +// ChangeSkillInfo 定义请求或响应数据结构。 type ChangeSkillInfo struct { Head common.TomeeHeader `cmd:"2312" struc:"skip"` CatchTime uint32 `json:"catchTime"` @@ -74,6 +86,7 @@ type ChangeSkillInfo struct { ReplaceSkill uint32 `json:"replaceSkill"` } +// C2S_Skill_Sort 定义请求或响应数据结构。 type C2S_Skill_Sort struct { Head common.TomeeHeader `cmd:"2328" struc:"skip"` CapTm uint32 `json:"capTm"` diff --git a/logic/controller/inbound_user.go b/logic/controller/inbound_user.go index accd12855..c1e4fb58e 100644 --- a/logic/controller/inbound_user.go +++ b/logic/controller/inbound_user.go @@ -10,11 +10,13 @@ import ( "hash/crc32" ) +// MAIN_LOGIN_IN 定义请求或响应数据结构。 type MAIN_LOGIN_IN struct { Head common.TomeeHeader `cmd:"1001" struc:"skip"` Sid []byte `struc:"[16]byte"` } +// CheakSession 处理控制器请求。 func (l *MAIN_LOGIN_IN) CheakSession() (bool, uint32) { t1 := hex.EncodeToString(l.Sid) r, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", l.Head.UserID)) @@ -30,16 +32,19 @@ func (l *MAIN_LOGIN_IN) CheakSession() (bool, uint32) { return true, crcValue } +// SimUserInfoInboundInfo 定义请求或响应数据结构。 type SimUserInfoInboundInfo struct { Head common.TomeeHeader `cmd:"2051" struc:"skip"` UserId uint32 `fieldDescription:"米米号" uint:"true" codec:"true"` } +// MoreUserInfoInboundInfo 定义请求或响应数据结构。 type MoreUserInfoInboundInfo struct { Head common.TomeeHeader `cmd:"2052" struc:"skip"` UserId uint32 `fieldDescription:"米米号" uint:"true" codec:"true"` } +// AimatInboundInfo 定义请求或响应数据结构。 type AimatInboundInfo struct { Head common.TomeeHeader `cmd:"2104" struc:"skip"` ItemId uint32 `description:"物品id 射击激光 物品id为0" codec:"auto" uint:"true"` @@ -47,6 +52,7 @@ type AimatInboundInfo struct { Point model.Pos `description:"射击的坐标 x y" codec:"auto"` } +// ChatInboundInfo 定义请求或响应数据结构。 type ChatInboundInfo struct { Head common.TomeeHeader `cmd:"2102" struc:"skip"` Reserve uint32 `json:"reserve" fieldDescription:"填充 默认值为0" uint:"true"` @@ -54,43 +60,51 @@ type ChatInboundInfo struct { Message string `json:"message" fieldDescription:"消息内容, 结束符为utf-8的数字0"` } +// ChangeColorInboundInfo 定义请求或响应数据结构。 type ChangeColorInboundInfo struct { Head common.TomeeHeader `cmd:"2063" struc:"skip"` Color uint32 `codec:"color"` } +// ChangeDoodleInboundInfo 定义请求或响应数据结构。 type ChangeDoodleInboundInfo struct { Head common.TomeeHeader `cmd:"2062" struc:"skip"` Id uint32 `codec:"id"` Color uint32 `codec:"color"` } +// ChangeNONOColorInboundInfo 定义请求或响应数据结构。 type ChangeNONOColorInboundInfo struct { Head common.TomeeHeader `cmd:"9012" struc:"skip"` Color uint32 `codec:"color"` } +// C2SDanceAction 定义请求或响应数据结构。 type C2SDanceAction struct { Head common.TomeeHeader `cmd:"2103" struc:"skip"` Reserve uint32 `struc:"uint32,big"` Type uint32 `struc:"uint32,big"` } +// C2SPEOPLE_TRANSFROM 定义请求或响应数据结构。 type C2SPEOPLE_TRANSFROM struct { Head common.TomeeHeader `cmd:"2111" struc:"skip"` SuitID uint32 `struc:"uint32,big"` } +// ChangePlayerNameInboundInfo 定义请求或响应数据结构。 type ChangePlayerNameInboundInfo struct { Head common.TomeeHeader `cmd:"2061" struc:"skip"` Nickname string `struc:"[16]byte"` } +// ChangeTitleInboundInfo 定义请求或响应数据结构。 type ChangeTitleInboundInfo struct { Head common.TomeeHeader `cmd:"3404" struc:"skip"` TileID uint32 } +// C2S_GET_GIFT_COMPLETE 定义请求或响应数据结构。 type C2S_GET_GIFT_COMPLETE struct { Head common.TomeeHeader `cmd:"2801" struc:"skip"` PassText string `struc:"[16]byte"` diff --git a/logic/controller/item_use.go b/logic/controller/item_use.go index 3d403fb53..874415e9b 100644 --- a/logic/controller/item_use.go +++ b/logic/controller/item_use.go @@ -43,12 +43,20 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c return nil, errorcode.ErrorCodes.ErrInsufficientItems } + oldHP := currentPet.Hp itemCfg, ok := xmlres.ItemsMAP[int(itemID)] if !ok { - return nil, errorcode.ErrorCodes.ErrSystemError + errcode := h.handleRegularPetItem(itemID, currentPet) + if errcode != 0 { + return nil, errcode + } + refreshPetPaneKeepHP(currentPet, oldHP) + c.Service.Item.UPDATE(itemID, -1) + result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{} + copier.Copy(&result, currentPet) + return result, 0 } - oldHP := currentPet.Hp var errcode errorcode.ErrorCode switch { case itemID == 300036: @@ -166,14 +174,7 @@ func refreshPetPaneKeepHP(currentPet *model.PetInfo, hp uint32) { // handleRegularPetItem 处理普通宠物道具 func (h Controller) handleRegularPetItem(itemID uint32, currentPet *model.PetInfo) errorcode.ErrorCode { - handler := item.PetItemRegistry.GetHandler(itemID) - if handler == nil { - return errorcode.ErrorCodes.ErrItemUnusable - } - if !handler(itemID, currentPet) { - return errorcode.ErrorCodes.ErrItemUnusable - } - return 0 + return item.PetItemRegistry.Handle(itemID, currentPet) } // ResetNature 重置宠物性格 diff --git a/logic/controller/map.go b/logic/controller/map.go index c145ad815..47cc797f9 100644 --- a/logic/controller/map.go +++ b/logic/controller/map.go @@ -39,6 +39,7 @@ func (h Controller) EnterMap(data *EnterMapInboundInfo, c *player.Player) (resul return nil, -1 } +// GetMapHot 处理控制器请求。 func (h Controller) GetMapHot(data *GetMapHotInboundInfo, c *player.Player) (result *maphot.OutInfo, err errorcode.ErrorCode) { result = &maphot.OutInfo{ HotInfos: space.GetMapHot(), diff --git a/logic/controller/nono.go b/logic/controller/nono.go index 5db4383cc..e420bc746 100644 --- a/logic/controller/nono.go +++ b/logic/controller/nono.go @@ -14,6 +14,7 @@ const ( nonoPetCureCost int64 = 50 ) +// NonoFollowOrHome 处理控制器请求。 func (h Controller) NonoFollowOrHome(data *NonoFollowOrHomeInInfo, c *player.Player) (result *nono.NonoFollowOutInfo, err errorcode.ErrorCode) { //这个时候player应该是空的 c.Info.NONO.Flag = data.Flag result = &nono.NonoFollowOutInfo{ @@ -49,6 +50,7 @@ func (h *Controller) GetNonoInfo(data *NonoInboundInfo, c *player.Player) (resul return } +// SwitchFlying 处理控制器请求。 func (h *Controller) SwitchFlying(data *SwitchFlyingInboundInfo, c *player.Player) (result *nono.SwitchFlyingOutboundInfo, err errorcode.ErrorCode) { //这个时候player应该是空的 result = &nono.SwitchFlyingOutboundInfo{ UserId: data.Head.UserID, @@ -59,6 +61,7 @@ func (h *Controller) SwitchFlying(data *SwitchFlyingInboundInfo, c *player.Playe return } +// PlayerPetCure 处理控制器请求。 func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (result *nono.PetCureOutboundEmpty, err errorcode.ErrorCode) { //这个时候player应该是空的 _ = data if c.IsArenaHealLocked() { diff --git a/logic/controller/pet_elo.go b/logic/controller/pet_elo.go index 7fdc7668b..42b51240e 100644 --- a/logic/controller/pet_elo.go +++ b/logic/controller/pet_elo.go @@ -12,6 +12,7 @@ import ( "github.com/jinzhu/copier" ) +// PetELV 处理控制器请求。 func (h Controller) PetELV(data *C2S_PET_EVOLVTION, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { _, currentPet, found := c.FindPet(data.CacthTime) if !found { diff --git a/logic/controller/pet_ev.go b/logic/controller/pet_ev.go index eabf6cdd3..3faac5e83 100644 --- a/logic/controller/pet_ev.go +++ b/logic/controller/pet_ev.go @@ -63,6 +63,7 @@ func (h Controller) PetEVDiy(data *PetEV, c *player.Player) (result *fight.NullO return result, 0 } +// PetEV 定义请求或响应数据结构。 type PetEV struct { Head common.TomeeHeader `cmd:"50001" struc:"skip"` CacthTime uint32 `description:"捕捉时间" codec:"cacthTime"` diff --git a/logic/controller/pet_ext.go b/logic/controller/pet_ext.go index d0fef2b2b..b709dc0c1 100644 --- a/logic/controller/pet_ext.go +++ b/logic/controller/pet_ext.go @@ -16,6 +16,7 @@ func (h Controller) PetExt( } +// C2S_NONO_EXE_LIST 定义请求或响应数据结构。 type C2S_NONO_EXE_LIST struct { Head common.TomeeHeader `cmd:"9015" struc:"skip"` } diff --git a/logic/controller/pet_fusion.go b/logic/controller/pet_fusion.go index f4e8a5ca1..395959bf3 100644 --- a/logic/controller/pet_fusion.go +++ b/logic/controller/pet_fusion.go @@ -19,6 +19,7 @@ const ( petFusionSoulID = 1000017 ) +// PetFusion 处理控制器请求。 func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pet.PetFusionInfo, err errorcode.ErrorCode) { result = &pet.PetFusionInfo{ SoulID: petFusionSoulID, diff --git a/logic/controller/pet_info.go b/logic/controller/pet_info.go index 20892ec88..e5f616fec 100644 --- a/logic/controller/pet_info.go +++ b/logic/controller/pet_info.go @@ -36,6 +36,7 @@ func (h Controller) GetUserBagPetInfo( return player.GetUserBagPetInfo(), 0 } +// GetPetListInboundEmpty 定义请求或响应数据结构。 type GetPetListInboundEmpty struct { Head common.TomeeHeader `cmd:"2303" struc:"skip"` } @@ -48,6 +49,7 @@ func (h Controller) GetPetList( return buildPetListOutboundInfo(player.Info.PetList), 0 } +// GetPetListFreeInboundEmpty 定义请求或响应数据结构。 type GetPetListFreeInboundEmpty struct { Head common.TomeeHeader `cmd:"2320" struc:"skip"` } diff --git a/logic/controller/pet_收集计划.go b/logic/controller/pet_收集计划.go index cba1e4989..993b95e10 100644 --- a/logic/controller/pet_收集计划.go +++ b/logic/controller/pet_收集计划.go @@ -10,6 +10,7 @@ import ( "github.com/samber/lo" ) +// IsCollect 处理控制器请求。 func (h Controller) IsCollect( data *pet.C2S_IS_COLLECT, c *player.Player) (result *pet.S2C_IS_COLLECT, err errorcode.ErrorCode) { //这个时候player应该是空的 result = &pet.S2C_IS_COLLECT{ @@ -49,6 +50,7 @@ var validTypeIDMap = map[int][]uint32{ 100: {856, 857, 858}, //测试 } +// Collect 处理控制器请求。 func (h Controller) Collect( data *pet.C2S_PET_COLLECT, c *player.Player) (result *pet.S2C_PET_COLLECT, err errorcode.ErrorCode) { //这个时候player应该是空的 result = &pet.S2C_PET_COLLECT{ID: data.ID} diff --git a/logic/controller/pet_繁殖.go b/logic/controller/pet_繁殖.go index 48f624af5..441e67ffd 100644 --- a/logic/controller/pet_繁殖.go +++ b/logic/controller/pet_繁殖.go @@ -120,6 +120,7 @@ func canBreedPair(maleID, femaleID uint32) bool { return ok } +// GetEggList 处理控制器请求。 func (ctl Controller) GetEggList( data *pet.C2S_GET_EGG_LIST, player *player.Player) (result *pet.S2C_GET_EGG_LIST, err errorcode.ErrorCode) { //这个时候player应该是空的 diff --git a/logic/controller/systemtime.go b/logic/controller/systemtime.go index bceeb7b81..5d9e338b1 100644 --- a/logic/controller/systemtime.go +++ b/logic/controller/systemtime.go @@ -8,6 +8,7 @@ import ( "blazing/logic/service/player" ) +// SystemTimeInfo 处理控制器请求。 func (h Controller) SystemTimeInfo(data *InInfo, c *player.Player) (result *OutInfo, err errorcode.ErrorCode) { return &OutInfo{ diff --git a/logic/controller/user_action.go b/logic/controller/user_action.go index 019cee9af..0125aafa7 100644 --- a/logic/controller/user_action.go +++ b/logic/controller/user_action.go @@ -171,6 +171,7 @@ func (h Controller) ChangePlayerCloth(data *ChangePlayerClothInboundInfo, player return } +// ChangePlayerName 处理控制器请求。 func (h Controller) ChangePlayerName(data *ChangePlayerNameInboundInfo, c *player.Player) (result *user.ChangePlayerNameOutboundInfo, err errorcode.ErrorCode) { newNickname := cool.Filter.Replace(strings.Trim(data.Nickname, "\x00"), '*') @@ -183,6 +184,8 @@ func (h Controller) ChangePlayerName(data *ChangePlayerNameInboundInfo, c *playe return result, 0 } + +// ChangeTile 处理控制器请求。 func (h Controller) ChangeTile(data *ChangeTitleInboundInfo, c *player.Player) (result *user.ChangeTitleOutboundInfo, err errorcode.ErrorCode) { result = &user.ChangeTitleOutboundInfo{ diff --git a/logic/controller/user_cdk.go b/logic/controller/user_cdk.go index 738b135ba..f2a96a4a1 100644 --- a/logic/controller/user_cdk.go +++ b/logic/controller/user_cdk.go @@ -2,19 +2,18 @@ package controller import ( "blazing/common/socket/errorcode" - "blazing/logic/service/player" + logicplayer "blazing/logic/service/player" "blazing/logic/service/user" - "blazing/modules/config/service" - "blazing/modules/player/model" + configservice "blazing/modules/config/service" + playerservice "blazing/modules/player/service" "time" ) -func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *player.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) { +// CDK 处理控制器请求。 +func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) { result = &user.S2C_GET_GIFT_COMPLETE{} - cdkService := service.NewCdkService() - rewardPetService := service.NewPetRewardService() - itemRewardService := service.NewItemService() + cdkService := configservice.NewCdkService() now := time.Now() r := cdkService.Get(data.PassText) @@ -24,7 +23,6 @@ func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *player.Player) (res if r.BindUserId != 0 && r.BindUserId != data.Head.UserID { return nil, errorcode.ErrorCodes.ErrMolecularCodeFrozen } - if r.ValidEndTime.Compare(now) == -1 { return nil, errorcode.ErrorCodes.ErrMolecularCodeExpired } @@ -35,28 +33,33 @@ func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *player.Player) (res return nil, errorcode.ErrorCodes.ErrMolecularCodeGiftsGone } + reward, grantErr := playerservice.NewCdkService(data.Head.UserID).GrantConfigReward(uint32(r.ID)) + if grantErr != nil { + return nil, errorcode.ErrorCodes.ErrSystemError + } + result.Flag = 1 - for _, rewardID := range r.ElfRewardIds { - pet := rewardPetService.Get(rewardID) - if pet == nil { - continue + appendGift := func(giftID, count int64) { + if giftID == 0 || count <= 0 { + return } - - petInfo := model.GenPetInfo(int(pet.MonID), int(pet.DV), int(pet.Nature), int(pet.Effect), int(pet.Lv), nil, 0) - player.Service.Pet.PetAdd(petInfo, 0) - result.PetGift = append(result.PetGift, user.PetGiftInfo{PetID: petInfo.ID, CacthTime: petInfo.CatchTime}) + result.GiftList = append(result.GiftList, user.GiftInfo{GiftID: giftID, Count: count}) } - for _, rewardID := range r.ItemRewardIds { - itemInfo := itemRewardService.GetItemCount(rewardID) - player.ItemAdd(itemInfo.ItemId, itemInfo.ItemCnt) - result.GiftList = append(result.GiftList, user.GiftInfo{GiftID: itemInfo.ItemId, Count: itemInfo.ItemCnt}) + appendGift(1, reward.Coins) + appendGift(3, reward.ExpPool) + appendGift(5, reward.Gold) + appendGift(9, reward.EVPool) + for _, item := range reward.Items { + appendGift(item.ItemId, item.ItemCnt) } - if r.TitleRewardIds != 0 { - player.Service.Title.Give(r.TitleRewardIds) - result.Tile = r.TitleRewardIds + for _, pet := range reward.Pets { + result.PetGift = append(result.PetGift, user.PetGiftInfo{PetID: pet.PetID, CacthTime: pet.CatchTime}) } + if len(reward.TitleIDs) > 0 { + result.Tile = reward.TitleIDs[0] + } + player.Service.Cdk.Log(uint32(r.ID)) - return } diff --git a/logic/controller/user_talk.go b/logic/controller/user_talk.go index 5b5a87e19..80c488caa 100644 --- a/logic/controller/user_talk.go +++ b/logic/controller/user_talk.go @@ -7,6 +7,7 @@ import ( "blazing/modules/config/service" ) +// GetTalkCount 处理控制器请求。 func (h Controller) GetTalkCount(data *TalkCountInboundInfo, c *player.Player) (result *item.TalkCountOutboundInfo, err errorcode.ErrorCode) { result = &item.TalkCountOutboundInfo{} talkCount, ok := c.Service.Talk.Cheak(c.Info.MapID, int(data.ID)) diff --git a/logic/controller/walk.go b/logic/controller/walk.go index baebcf6c4..3269578ef 100644 --- a/logic/controller/walk.go +++ b/logic/controller/walk.go @@ -6,6 +6,7 @@ import ( "blazing/logic/service/space/info" ) +// PlayerWalk 处理控制器请求。 func (h Controller) PlayerWalk(data *WalkInInfo, c *player.Player) (result *info.WalkOutInfo, err errorcode.ErrorCode) { result = &info.WalkOutInfo{ Flag: data.Flag, 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/common/fight.go b/logic/service/common/fight.go index 0e72c20f5..44f1f1f1a 100644 --- a/logic/service/common/fight.go +++ b/logic/service/common/fight.go @@ -5,6 +5,7 @@ import ( "blazing/modules/player/model" ) +// FightI 定义 common 服务层依赖的战斗操作接口。 type FightI interface { Over(c PlayerI, id model.EnumBattleOverReason) //逃跑 UseSkill(c PlayerI, id uint32) //使用技能 diff --git a/logic/service/common/log.go b/logic/service/common/log.go index d18956bfc..616b5f1d2 100644 --- a/logic/service/common/log.go +++ b/logic/service/common/log.go @@ -7,24 +7,25 @@ import ( "github.com/gogf/gf/v2/os/glog" ) +// MyWriter 自定义日志写入器,用于逻辑服日志转发。 type MyWriter struct { - logger *glog.Logger - user uint32 + logger *glog.Logger // 底层 glog 实例。 + user uint32 // 关联的玩家 ID。 } +// Write 实现 io.Writer,并将日志写入系统日志与底层 logger。 func (w *MyWriter) Write(p []byte) (n int, err error) { var ( s = string(p) - //ctx = context.Background() ) service.NewBaseSysLogService().RecordLog(w.user, s) return w.logger.Write(p) } + func init() { cool.Logger.SetWriter(&MyWriter{ logger: glog.New(), }) cool.Logger.SetAsync(true) - } diff --git a/logic/service/common/pack.go b/logic/service/common/pack.go index 520f8aee9..a98b79f5c 100644 --- a/logic/service/common/pack.go +++ b/logic/service/common/pack.go @@ -8,20 +8,18 @@ import ( "github.com/lunixbochs/struc" ) -// TomeeHeader 结构体字段定义 +// TomeeHeader 定义协议包头。 type TomeeHeader struct { - Len uint32 `json:"len"` - Version byte `json:"version" struc:"[1]byte"` - CMD uint32 `json:"cmdId" struc:"uint32"` - UserID uint32 `json:"userId"` - //Error uint32 `json:"error" struc:"skip"` - - Result uint32 `json:"result"` - Data []byte `json:"data" struc:"skip"` //组包忽略此字段// struc:"skip" - Res []byte `struc:"skip"` - //Return []byte `struc:"skip"` //返回记录 + Len uint32 `json:"len"` // 包总长度(包头 + 数据体)。 + Version byte `json:"version" struc:"[1]byte"` // 协议版本。 + CMD uint32 `json:"cmdId" struc:"uint32"` // 命令 ID。 + UserID uint32 `json:"userId"` // 玩家 ID。 + Result uint32 `json:"result"` // 结果码。 + Data []byte `json:"data" struc:"skip"` // 数据体,序列化时跳过。 + Res []byte `struc:"skip"` // 预留返回数据,序列化时跳过。 } +// NewTomeeHeader 创建用于下行封包的默认 TomeeHeader。 func NewTomeeHeader(cmd uint32, userid uint32) *TomeeHeader { return &TomeeHeader{ diff --git a/logic/service/common/playeri.go b/logic/service/common/playeri.go index d67416271..58b2ca6e5 100644 --- a/logic/service/common/playeri.go +++ b/logic/service/common/playeri.go @@ -7,6 +7,7 @@ import ( "blazing/modules/player/model" ) +// PlayerI 定义战斗与 common 服务依赖的最小玩家能力接口。 type PlayerI interface { ApplyPetDisplayInfo(*space.SimpleInfo) GetPlayerCaptureContext() *info.PlayerCaptureContext diff --git a/logic/service/fight/boss/NewSeIdx_247.go b/logic/service/fight/boss/NewSeIdx_247.go new file mode 100644 index 000000000..96ea9ed8b --- /dev/null +++ b/logic/service/fight/boss/NewSeIdx_247.go @@ -0,0 +1,99 @@ +package effect + +import ( + "blazing/logic/service/fight/action" + "blazing/logic/service/fight/info" + "blazing/logic/service/fight/input" +) + +// 247. 固定增加体力/攻击/防御/特攻/特防/速度;(a1-a6: hp/atk/def/spatk/spdef/spd) +type NewSel247 struct { + NewSel0 +} + +func (e *NewSel247) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) { + if !e.IsOwner() { + return + } + + pet := e.Ctx().Our.CurPet[0] + if pet == nil { + return + } + + hpBonus := uint32(e.Args()[0].IntPart()) + if hpBonus > 0 { + pet.Info.MaxHp += hpBonus + pet.Info.Hp += hpBonus + } + + for i, propIdx := range []int{0, 1, 2, 3, 4} { + add := uint32(e.Args()[i+1].IntPart()) + if add == 0 { + continue + } + pet.Info.Prop[propIdx] += add + } +} + +func (e *NewSel247) TurnEnd() { + if !e.IsOwner() { + return + } + + pet := e.Ctx().Our.CurPet[0] + if pet == nil { + return + } + + hpBonus := uint32(e.Args()[0].IntPart()) + if hpBonus > 0 { + if pet.Info.MaxHp > hpBonus { + pet.Info.MaxHp -= hpBonus + } else { + pet.Info.MaxHp = 1 + } + if pet.Info.Hp > pet.Info.MaxHp { + pet.Info.Hp = pet.Info.MaxHp + } + } + + for i, propIdx := range []int{0, 1, 2, 3, 4} { + sub := uint32(e.Args()[i+1].IntPart()) + if sub == 0 { + continue + } + if pet.Info.Prop[propIdx] > sub { + pet.Info.Prop[propIdx] -= sub + } else { + pet.Info.Prop[propIdx] = 1 + } + } +} + +type NewSel239 struct { + NewSel0 +} + +func (e *NewSel239) ActionStart(a, b *action.SelectSkillAction) bool { + if !e.IsOwner() { + return true + } + if e.Ctx().SkillEntity == nil { + return true + } + if e.Ctx().SkillEntity.Category() == info.Category.STATUS { + return true + } + if len(e.Args()) == 0 { + return true + } + + e.Ctx().SkillEntity.XML.Power += int(e.Args()[0].IntPart()) + return true +} + +func init() { + input.InitEffect(input.EffectType.NewSel, 239, &NewSel239{}) + input.InitEffect(input.EffectType.NewSel, 247, &NewSel247{}) +} diff --git a/logic/service/fight/boss/NewSeIdx_26.go b/logic/service/fight/boss/NewSeIdx_26.go index d22172f5b..df68ea7ca 100644 --- a/logic/service/fight/boss/NewSeIdx_26.go +++ b/logic/service/fight/boss/NewSeIdx_26.go @@ -11,10 +11,9 @@ type NewSel26 struct { } func (e *NewSel26) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) { - e.Ctx().Our.CurPet[0].Info.Prop[int(e.Args()[0].IntPart())] += uint32(e.Args()[1].IntPart()) } + func (e *NewSel26) TurnEnd() { - e.Ctx().Our.CurPet[0].Info.Prop[int(e.Args()[0].IntPart())] -= uint32(e.Args()[1].IntPart()) } func init() { input.InitEffect(input.EffectType.NewSel, 26, &NewSel26{}) diff --git a/logic/service/fight/boss/NewSeIdx_501.go b/logic/service/fight/boss/NewSeIdx_501.go index 0007755c4..4d360c9a7 100644 --- a/logic/service/fight/boss/NewSeIdx_501.go +++ b/logic/service/fight/boss/NewSeIdx_501.go @@ -1,26 +1,25 @@ package effect -import ( - "blazing/logic/service/fight/input" -) +import "blazing/logic/service/fight/input" // 501. g1. 最后一个死 (只要有队友没死, 则自己又恢复hp和pp) -// TODO: 需要了解如何判断队友状态并恢复HP和PP type NewSel501 struct { NewSel0 } +// SwitchOut 在拥有者死亡离场时触发;若仍有存活队友,则把自己回满留作后续再上场。 func (e *NewSel501) SwitchOut(in *input.Input) bool { - //魂印特性有不在场的情况,绑定时候将精灵和特性绑定 - if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime { + owner := e.SourceInput() + if owner == nil || in != owner || !e.IsOwner() { + return true + } + currentPet := owner.CurrentPet() + if currentPet == nil || currentPet.Info.Hp > 0 || !owner.HasLivingTeammate() { return true } - // TODO: 检查是否有队友还活着 - // 如果有队友活着,恢复自身HP和PP - // e.Ctx().Our.Heal(e.Ctx().Our, nil, e.Ctx().Our.CurPet[0].GetMaxHP()) - // TODO: 恢复PP值的方法 - + currentPet.Info.Hp = currentPet.Info.MaxHp + owner.HealPP(-1) return true } diff --git a/logic/service/fight/boss/NewSeIdx_502.go b/logic/service/fight/boss/NewSeIdx_502.go index 66bf26fc0..a77d2f579 100644 --- a/logic/service/fight/boss/NewSeIdx_502.go +++ b/logic/service/fight/boss/NewSeIdx_502.go @@ -1,25 +1,31 @@ package effect -import ( - "blazing/logic/service/fight/input" -) +import "blazing/logic/service/fight/input" // 502. g2. 如果自身死亡, 恢复队友所有体力和PP值 -// TODO: 实现恢复队友所有体力和PP值的核心逻辑 type NewSel502 struct { NewSel0 } -// TODO: 需要找到精灵死亡时的回调接口 +// SwitchOut 在拥有者死亡离场时触发;把仍在场的队友体力和 PP 恢复到满值。 func (e *NewSel502) SwitchOut(in *input.Input) bool { - //魂印特性有不在场的情况,绑定时候将精灵和特性绑定 - if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime { + owner := e.SourceInput() + if owner == nil || in != owner || !e.IsOwner() { + return true + } + currentPet := owner.CurrentPet() + if currentPet == nil || currentPet.Info.Hp > 0 { return true } - // TODO: 检查精灵是否死亡(HP为0) - // 如果死亡,恢复队友所有体力和PP值 - // 需要遍历队友的精灵并调用相应的方法 + for _, teammate := range owner.LivingTeammates() { + pet := teammate.CurrentPet() + if pet == nil { + continue + } + pet.Info.Hp = pet.Info.MaxHp + teammate.HealPP(-1) + } return true } diff --git a/logic/service/fight/boss/NewSeIdx_503.go b/logic/service/fight/boss/NewSeIdx_503.go index 35fa97908..781e1e3dc 100644 --- a/logic/service/fight/boss/NewSeIdx_503.go +++ b/logic/service/fight/boss/NewSeIdx_503.go @@ -6,20 +6,35 @@ import ( ) // 503. g3. 群体攻击技能可额外增加一个目标(最多不超过5个目标) -// TODO: 需要了解如何修改群体攻击技能的目标数量 type NewSel503 struct { NewSel0 } +// TurnStart 在拥有者本回合准备出手时触发;若本次技能是群体技能,则把目标数额外加 1。 func (e *NewSel503) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) { - //魂印特性有不在场的情况,绑定时候将精灵和特性绑定 - if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime { + owner := e.SourceInput() + if owner == nil || !e.IsOwner() { return } - // TODO: 检查技能是否是群体攻击技能 - // 如果是群体攻击,增加一个目标(最多不超过5个) - // 需要了解技能的目标数量限制机制 + for _, act := range []*action.SelectSkillAction{fattack, sattack} { + if act == nil || act.SkillEntity == nil || act.SkillEntity.Pet == nil { + continue + } + if act.SkillEntity.Pet.Info.CatchTime != e.ID().GetCatchTime() { + continue + } + if act.SkillEntity.XML.AtkType != 0 { + return + } + if act.SkillEntity.XML.AtkNum <= 0 { + act.SkillEntity.XML.AtkNum = 1 + } + if act.SkillEntity.XML.AtkNum < 5 { + act.SkillEntity.XML.AtkNum++ + } + return + } } func init() { diff --git a/logic/service/fight/cmd_unified.go b/logic/service/fight/cmd_unified.go new file mode 100644 index 000000000..6352b4aa6 --- /dev/null +++ b/logic/service/fight/cmd_unified.go @@ -0,0 +1,148 @@ +package fight + +import ( + "blazing/logic/service/common" + "blazing/modules/player/model" +) + +// FightActionType 表示统一动作包里的动作类型。 +type FightActionType string + +const ( + // FightActionTypeSkill 表示使用技能。 + FightActionTypeSkill FightActionType = "skill" + // FightActionTypeItem 表示使用道具。 + FightActionTypeItem FightActionType = "item" + // FightActionTypeChange 表示主动切宠。 + FightActionTypeChange FightActionType = "change" + // FightActionTypeEscape 表示逃跑。 + FightActionTypeEscape FightActionType = "escape" + // FightActionTypeChat 表示战斗内聊天。 + FightActionTypeChat FightActionType = "chat" +) + +// FightActionEnvelope 是统一入站动作结构。 +// 约定: +// 1. actorIndex 始终表示发起方所在的我方槽位。 +// 2. targetIndex 始终表示目标在所属阵营内的槽位。 +// 3. targetRelation 用来区分 targetIndex 属于敌方、自己还是队友。 +type FightActionEnvelope struct { + // ActionType 当前动作类型,例如 skill、item、change、escape、chat。 + ActionType FightActionType `json:"actionType"` + // ActorIndex 发起动作的我方槽位。 + ActorIndex int `json:"actorIndex"` + // TargetIndex 目标在所属阵营中的槽位下标。 + TargetIndex int `json:"targetIndex"` + // TargetRelation 目标关系:0=对方,1=自己,2=队友。 + TargetRelation uint8 `json:"targetRelation,omitempty"` + // SkillID 技能 ID;仅技能动作使用。 + SkillID uint32 `json:"skillId,omitempty"` + // ItemID 道具 ID;仅道具动作使用。 + ItemID uint32 `json:"itemId,omitempty"` + // CatchTime 精灵实例 ID;切宠或部分道具动作使用。 + CatchTime uint32 `json:"catchTime,omitempty"` + // Escape 是否为逃跑动作;主要用于协议层兼容和调试。 + Escape bool `json:"escape,omitempty"` + // Chat 聊天内容;仅聊天动作使用。 + Chat string `json:"chat,omitempty"` + // AtkType 前端技能目标类型兜底值,沿用技能表 AtkType 定义。 + AtkType uint8 `json:"atkType,omitempty"` +} + +// NewSkillActionEnvelope 构造技能动作 envelope。 +func NewSkillActionEnvelope(skillID uint32, actorIndex, targetIndex int, targetRelation, atkType uint8) FightActionEnvelope { + return FightActionEnvelope{ + ActionType: FightActionTypeSkill, + ActorIndex: actorIndex, + TargetIndex: targetIndex, + TargetRelation: targetRelation, + SkillID: skillID, + AtkType: atkType, + } +} + +// NewItemActionEnvelope 构造道具动作 envelope。 +func NewItemActionEnvelope(catchTime, itemID uint32, actorIndex, targetIndex int, targetRelation uint8) FightActionEnvelope { + return FightActionEnvelope{ + ActionType: FightActionTypeItem, + ActorIndex: actorIndex, + TargetIndex: targetIndex, + TargetRelation: targetRelation, + ItemID: itemID, + CatchTime: catchTime, + } +} + +// NewChangeActionEnvelope 构造切宠动作 envelope。 +func NewChangeActionEnvelope(catchTime uint32, actorIndex int) FightActionEnvelope { + return FightActionEnvelope{ + ActionType: FightActionTypeChange, + ActorIndex: actorIndex, + CatchTime: catchTime, + } +} + +// NewEscapeActionEnvelope 构造逃跑动作 envelope。 +func NewEscapeActionEnvelope() FightActionEnvelope { + return FightActionEnvelope{ + ActionType: FightActionTypeEscape, + Escape: true, + } +} + +// NewChatActionEnvelope 构造聊天动作 envelope。 +func NewChatActionEnvelope(chat string) FightActionEnvelope { + return FightActionEnvelope{ + ActionType: FightActionTypeChat, + Chat: chat, + } +} + +// normalizedTargetRelation 根据 TargetRelation 和 AtkType 兜底出最终目标关系。 +func (e FightActionEnvelope) normalizedTargetRelation() uint8 { + if e.TargetRelation <= SkillTargetAlly { + return e.TargetRelation + } + switch e.AtkType { + case 3: + return SkillTargetSelf + case 1: + return SkillTargetAlly + default: + return SkillTargetOpponent + } +} + +// EncodedTargetIndex 把统一结构里的目标信息编码成现有 FightC 内部使用的目标格式。 +func (e FightActionEnvelope) EncodedTargetIndex() int { + targetIndex := e.TargetIndex + switch e.normalizedTargetRelation() { + case SkillTargetSelf: + targetIndex = e.ActorIndex + return EncodeTargetIndex(targetIndex, false) + case SkillTargetAlly: + return EncodeTargetIndex(targetIndex, false) + default: + return EncodeTargetIndex(targetIndex, true) + } +} + +// HandleActionEnvelope 把统一动作结构派发到现有 FightC 的 indexed 接口上。 +func (f *FightC) HandleActionEnvelope(c common.PlayerI, envelope FightActionEnvelope) { + if f == nil || c == nil { + return + } + + switch envelope.ActionType { + case FightActionTypeSkill: + f.UseSkillAt(c, envelope.SkillID, envelope.ActorIndex, envelope.EncodedTargetIndex()) + case FightActionTypeItem: + f.UseItemAt(c, envelope.CatchTime, envelope.ItemID, envelope.ActorIndex, envelope.EncodedTargetIndex()) + case FightActionTypeChange: + f.ChangePetAt(c, envelope.CatchTime, envelope.ActorIndex) + case FightActionTypeEscape: + f.Over(c, model.BattleOverReason.PlayerEscape) + case FightActionTypeChat: + f.Chat(c, envelope.Chat) + } +} diff --git a/logic/service/fight/info/BattlePetEntity.go b/logic/service/fight/info/BattlePetEntity.go index fb26494c9..cbe2b98b0 100644 --- a/logic/service/fight/info/BattlePetEntity.go +++ b/logic/service/fight/info/BattlePetEntity.go @@ -41,6 +41,35 @@ func (t *BattlePetEntity) Alive() bool { } +func (t *BattlePetEntity) AddBattleAttr(attr int, value uint32) { + if t == nil || value == 0 { + return + } + + switch attr { + case 0: + t.Info.MaxHp += value + t.Info.Hp += value + case 1, 2, 3, 4, 5: + t.Info.Prop[attr-1] += value + } +} + +func (t *BattlePetEntity) ApplyInitEffectBonus(effect model.PetEffectInfo) { + if t == nil || effect.EID != 26 || len(effect.Args) < 2 { + return + } + + for i := 0; i+1 < len(effect.Args); i += 2 { + attr := effect.Args[i] + value := effect.Args[i+1] + if value <= 0 { + continue + } + t.AddBattleAttr(attr, uint32(value)) + } +} + // 创建精灵实例 func CreateBattlePetEntity(info model.PetInfo) *BattlePetEntity { ret := &BattlePetEntity{} diff --git a/logic/service/fight/info/unified_info.go b/logic/service/fight/info/unified_info.go new file mode 100644 index 000000000..5c07d7695 --- /dev/null +++ b/logic/service/fight/info/unified_info.go @@ -0,0 +1,114 @@ +package info + +import "blazing/modules/player/model" + +// FightStatePhase 表示统一战斗状态包的阶段。 +type FightStatePhase string + +const ( + // FightStatePhaseStart 表示开战快照。 + FightStatePhaseStart FightStatePhase = "start" + // FightStatePhaseSkillHurt 表示技能结算后的伤害快照。 + FightStatePhaseSkillHurt FightStatePhase = "skill_hurt" + // FightStatePhaseChange 表示切宠快照。 + FightStatePhaseChange FightStatePhase = "change" + // FightStatePhaseOver 表示战斗结束快照。 + FightStatePhaseOver FightStatePhase = "over" + // FightStatePhaseLoad 表示加载进度快照。 + FightStatePhaseLoad FightStatePhase = "load" + // FightStatePhaseChat 表示战斗内聊天快照。 + FightStatePhaseChat FightStatePhase = "chat" +) + +// FighterState 是统一的战位快照。 +// position 表示该 side 下的槽位编号;actorIndex/targetIndex 与其一一对应。 +type FighterState struct { + // Side 阵营标识:1=我方,2=敌方。 + Side int `json:"side"` + // Position 战位下标;在各自 side 内部从 0 开始编号。 + Position int `json:"position"` + // UserID 当前战位所属玩家 ID;野怪/NPC 通常为 0。 + UserID uint32 `json:"userId"` + // ControllerUserID 当前上场精灵的实际操作者 ID;组队时可与 UserID 联合定位操作者和战位归属。 + ControllerUserID uint32 `json:"controllerUserId"` + // PetID 当前上场精灵的物种/配置 ID。 + PetID uint32 `json:"petId"` + // CatchTime 当前上场精灵的唯一实例 ID,可理解为这只精灵在玩家背包中的唯一标识。 + CatchTime uint32 `json:"catchTime"` + // Name 当前上场精灵名字。 + Name string `json:"name,omitempty"` + // HP 当前生命值。 + HP uint32 `json:"hp"` + // MaxHP 最大生命值。 + MaxHP uint32 `json:"maxHp"` + // Level 当前等级。 + Level uint32 `json:"level"` + // Anger 怒气值;当前服务端主链路暂未实际填充时默认为 0,先为协议对齐预留。 + Anger uint32 `json:"anger"` + // Status 当前异常或增益状态回合数组;下标语义沿用现有战斗状态定义。 + Status [20]int8 `json:"status"` + // Prop 当前能力等级变化数组:攻击、防御、特攻、特防、速度、命中。 + Prop [6]int8 `json:"prop"` + // Skills 当前可见技能列表,包含技能 ID 和当前 PP 等信息。 + Skills []model.SkillInfo `json:"skills,omitempty"` +} + +// FightStateMeta 是统一状态包的公共元数据。 +type FightStateMeta struct { + // Round 当前回合数。 + Round uint32 `json:"round"` + // Weather 当前天气或场地编号;当前主链路未填充时可为 0。 + Weather uint32 `json:"weather,omitempty"` + // WinnerID 当前已确定的胜者 ID;未结束时通常为 0。 + WinnerID uint32 `json:"winnerId,omitempty"` + // Reason 当前已确定的结束原因;未结束时通常为 0。 + Reason model.EnumBattleOverReason `json:"reason,omitempty"` + // LegacyCmd 对应旧协议命令号,便于新旧包对照和过渡期调试。 + LegacyCmd uint32 `json:"legacyCmd,omitempty"` +} + +// FightSkillHurtState 保存技能结算阶段的详细战报。 +type FightSkillHurtState struct { + // Left 我方阵营本次技能结算后的攻击值快照列表。 + Left []model.AttackValue `json:"left,omitempty"` + // Right 敌方阵营本次技能结算后的攻击值快照列表。 + Right []model.AttackValue `json:"right,omitempty"` +} + +// FightLoadState 保存加载进度信息。 +type FightLoadState struct { + // UserID 当前上报加载进度的玩家 ID。 + UserID uint32 `json:"userId"` + // Percent 当前加载百分比。 + Percent uint32 `json:"percent"` +} + +// FightChatState 保存战斗内聊天信息。 +type FightChatState struct { + // SenderID 发言玩家 ID。 + SenderID uint32 `json:"senderId"` + // SenderNickname 发言玩家昵称。 + SenderNickname string `json:"senderNickname"` + // Message 聊天内容。 + Message string `json:"message"` +} + +// FightStateEnvelope 是统一出站状态结构。 +type FightStateEnvelope struct { + // Phase 当前下发阶段,例如 start、skill_hurt、change、over、load、chat。 + Phase FightStatePhase `json:"phase"` + // Left 我方阵营当前所有战位快照。 + Left []FighterState `json:"left,omitempty"` + // Right 敌方阵营当前所有战位快照。 + Right []FighterState `json:"right,omitempty"` + // Meta 当前阶段共用的元数据,如回合号、胜方、结束原因、旧协议命令号。 + Meta FightStateMeta `json:"meta"` + // SkillHurt 技能结算阶段附带的详细战报;仅 phase=skill_hurt 时使用。 + SkillHurt *FightSkillHurtState `json:"skillHurt,omitempty"` + // Change 切宠阶段附带的切宠详情;仅 phase=change 时使用。 + Change *ChangePetInfo `json:"change,omitempty"` + // Load 加载阶段附带的进度信息;仅 phase=load 时使用。 + Load *FightLoadState `json:"load,omitempty"` + // Chat 聊天阶段附带的聊天内容;仅 phase=chat 时使用。 + Chat *FightChatState `json:"chat,omitempty"` +} diff --git a/logic/service/fight/input/ai.go b/logic/service/fight/input/ai.go index 83bab2bbe..5ccca933a 100644 --- a/logic/service/fight/input/ai.go +++ b/logic/service/fight/input/ai.go @@ -2,51 +2,64 @@ package input import ( "blazing/logic/service/fight/info" + "blazing/logic/service/player" + configmodel "blazing/modules/config/model" + playermodel "blazing/modules/player/model" + "strings" "github.com/gogf/gf/v2/util/grand" ) // Shuffle 打乱切片顺序,使用 Fisher-Yates 洗牌算法,泛型支持任意类型 func Shuffle[T any](slice []T) { - // 从后往前遍历,逐个交换 for i := len(slice) - 1; i > 0; i-- { - // 生成 0 到 i 之间的随机索引 j := grand.Intn(i + 1) - // 交换 i 和 j 位置的元素 slice[i], slice[j] = slice[j], slice[i] } } + func (our *Input) GetAction() { - next := our.Exec(func(t Effect) bool { - return t.HookAction() }) + + scriptCtx := buildBossHookActionContext(our, next) + if aiPlayer, ok := our.Player.(*player.AI_player); ok && aiPlayer.BossScript != "" { + scriptBoss := &configmodel.BossConfig{Script: aiPlayer.BossScript} + nextByScript, err := scriptBoss.RunHookActionScript(scriptCtx) + if err != nil { + return + } + next = nextByScript + } + if !next { return } - // 获取己方当前宠物和对方当前宠物 + + if applyBossScriptAction(our, scriptCtx) { + return + } + selfPet := our.FightC.GetCurrPET(our.Player) - //没血就切换精灵 + if selfPet == nil { + return + } if selfPet.Info.Hp <= 0 { for _, v := range our.AllPet { if v.Info.Hp > 0 { - // println("AI出手,切换宠物") our.FightC.ChangePet(our.Player, v.Info.CatchTime) return } } - // 如果没有可用宠物,则直接返回,不执行任何操作 return } - //oppPet := opp.FightC.GetCurrPET(opp.Player) - skills := selfPet.Skills - // 空技能列表直接返回,避免错误 + skills := selfPet.Skills if len(skills) == 0 { return } - //println("AI出手,选择技能") + var usedskill *info.SkillEntity for _, s := range skills { if s == nil { @@ -55,30 +68,25 @@ func (our *Input) GetAction() { if !s.CanUse() { continue } - // 计算技能对对方的伤害(假设CalculatePower返回伤害值,或需从技能中获取) s.DamageValue = our.CalculatePower(our.Opp, s) oppPet := our.Opp.CurrentPet() if oppPet == nil { continue } - // 判断是否能秒杀(伤害 >= 对方当前生命值) - if s.DamageValue.Cmp(oppPet.GetHP()) != -1 { // 假设oppPet.HP为对方当前剩余生命值 - + if s.DamageValue.Cmp(oppPet.GetHP()) != -1 { if usedskill != nil { if s.DamageValue.Cmp(usedskill.DamageValue) != -1 { usedskill = s } - } else { usedskill = s } - } } + Shuffle(skills) if usedskill == nil { - for _, s := range skills { if s == nil { continue @@ -87,14 +95,121 @@ func (our *Input) GetAction() { continue } usedskill = s - } - } + if usedskill != nil { our.FightC.UseSkill(our.Player, uint32(usedskill.XML.ID)) } else { our.FightC.UseSkill(our.Player, 0) } - +} + +func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHookActionContext { + ctx := &configmodel.BossHookActionContext{ + HookAction: hookAction, + Action: "auto", + } + ctx.UseSkillFn = func(skillID uint32) { + ctx.Action = "skill" + ctx.SkillID = skillID + } + ctx.SwitchPetFn = func(catchTime uint32) { + ctx.Action = "switch" + ctx.CatchTime = catchTime + } + + if our == nil || our.FightC == nil || our.Player == nil { + return ctx + } + + overInfo := our.FightC.GetOverInfo() + ctx.Round = overInfo.Round + ctx.IsFirst = our.FightC.IsFirst(our.Player) + + if selfPet := our.CurrentPet(); selfPet != nil { + ctx.Our = &configmodel.BossHookPetContext{ + PetID: selfPet.Info.ID, + CatchTime: selfPet.Info.CatchTime, + Hp: selfPet.Info.Hp, + MaxHp: selfPet.Info.MaxHp, + } + ctx.Skills = make([]configmodel.BossHookSkillContext, 0, len(selfPet.Skills)) + for _, s := range selfPet.Skills { + if s == nil || s.Info == nil { + continue + } + ctx.Skills = append(ctx.Skills, configmodel.BossHookSkillContext{ + SkillID: s.Info.ID, + PP: s.Info.PP, + CanUse: s.CanUse(), + }) + } + } + + if our.AttackValue != nil { + ctx.OurAttack = convertAttackValue(our.AttackValue) + } + if our.Opp != nil { + if oppPet := our.Opp.CurrentPet(); oppPet != nil { + ctx.Opp = &configmodel.BossHookPetContext{ + PetID: oppPet.Info.ID, + CatchTime: oppPet.Info.CatchTime, + Hp: oppPet.Info.Hp, + MaxHp: oppPet.Info.MaxHp, + } + } + if our.Opp.AttackValue != nil { + ctx.OppAttack = convertAttackValue(our.Opp.AttackValue) + } + } + + return ctx +} + +func convertAttackValue(src *playermodel.AttackValue) *configmodel.BossHookAttackContext { + if src == nil { + return nil + } + status := make([]int8, len(src.Status)) + for i := range src.Status { + status[i] = src.Status[i] + } + prop := make([]int8, len(src.Prop)) + for i := range src.Prop { + prop[i] = src.Prop[i] + } + + return &configmodel.BossHookAttackContext{ + SkillID: src.SkillID, + AttackTime: src.AttackTime, + IsCritical: src.IsCritical, + LostHp: src.LostHp, + GainHp: src.GainHp, + RemainHp: src.RemainHp, + MaxHp: src.MaxHp, + State: src.State, + Offensive: src.Offensive, + Status: status, + Prop: prop, + } +} + +func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext) bool { + if our == nil || ctx == nil { + return false + } + + switch strings.ToLower(strings.TrimSpace(ctx.Action)) { + case "", "auto": + return false + case "skill", "use_skill", "useskill": + our.FightC.UseSkill(our.Player, ctx.SkillID) + return true + case "switch", "change_pet", "changepet": + our.FightC.ChangePet(our.Player, ctx.CatchTime) + return true + default: + return false + } } diff --git a/logic/service/fight/input/input.go b/logic/service/fight/input/input.go index 1b050c94a..7f19a2508 100644 --- a/logic/service/fight/input/input.go +++ b/logic/service/fight/input/input.go @@ -167,6 +167,7 @@ func (our *Input) SortPet() { t.Duration(-1) + s.ApplyInitEffectBonus(e1) our.AddEffect(our, t) } @@ -338,9 +339,6 @@ func (our *Input) Parseskill(skill *action.SelectSkillAction) { args := xmlres.EffectArgs[v] t := our.InitEffect(EffectType.Skill, v, temparg[:args]...) - - - //这里是给双方添加buff if t != nil { // t.SetArgs(our, temparg[:args]...) //设置入参,施加方永远是我方 diff --git a/logic/service/fight/input/team.go b/logic/service/fight/input/team.go new file mode 100644 index 000000000..f84a9090d --- /dev/null +++ b/logic/service/fight/input/team.go @@ -0,0 +1,55 @@ +package input + +// TeamSlots 返回当前输入所在阵营的全部有效战斗位视图。 +func (our *Input) TeamSlots() []*Input { + if our == nil { + return nil + } + if len(our.Team) == 0 { + return []*Input{our} + } + slots := make([]*Input, 0, len(our.Team)) + for _, teammate := range our.Team { + if teammate == nil { + continue + } + slots = append(slots, teammate) + } + return slots +} + +// Teammates 返回队友列表,不包含自己。 +func (our *Input) Teammates() []*Input { + if our == nil { + return nil + } + teammates := make([]*Input, 0, len(our.Team)) + for _, teammate := range our.TeamSlots() { + if teammate == nil || teammate == our { + continue + } + teammates = append(teammates, teammate) + } + return teammates +} + +// LivingTeammates 返回当前仍有存活出战精灵的队友列表。 +func (our *Input) LivingTeammates() []*Input { + if our == nil { + return nil + } + teammates := make([]*Input, 0, len(our.Team)) + for _, teammate := range our.Teammates() { + currentPet := teammate.CurrentPet() + if currentPet == nil || currentPet.Info.Hp == 0 { + continue + } + teammates = append(teammates, teammate) + } + return teammates +} + +// HasLivingTeammate 用于快速判断当前战斗位是否还有存活队友。 +func (our *Input) HasLivingTeammate() bool { + return len(our.LivingTeammates()) > 0 +} diff --git a/logic/service/fight/input/team_test.go b/logic/service/fight/input/team_test.go new file mode 100644 index 000000000..9059a99a3 --- /dev/null +++ b/logic/service/fight/input/team_test.go @@ -0,0 +1,30 @@ +package input + +import ( + "testing" + + fightinfo "blazing/logic/service/fight/info" + "blazing/modules/player/model" +) + +func TestLivingTeammatesFiltersSelfAndDeadSlots(t *testing.T) { + owner := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 10}}}} + aliveMate := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 5}}}} + deadMate := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 0}}}} + + team := []*Input{owner, aliveMate, deadMate} + owner.Team = team + aliveMate.Team = team + deadMate.Team = team + + teammates := owner.LivingTeammates() + if len(teammates) != 1 { + t.Fatalf("expected 1 living teammate, got %d", len(teammates)) + } + if teammates[0] != aliveMate { + t.Fatalf("expected alive teammate to be returned") + } + if owner.HasLivingTeammate() != true { + t.Fatalf("expected owner to detect living teammate") + } +} diff --git a/logic/service/fight/loop.go b/logic/service/fight/loop.go index 044b340de..c7c47e4b9 100644 --- a/logic/service/fight/loop.go +++ b/logic/service/fight/loop.go @@ -24,6 +24,26 @@ import ( "github.com/jinzhu/copier" ) +func consumeLimitedPetEffects(pet *model.PetInfo) { + if pet == nil || len(pet.EffectInfo) == 0 { + return + } + + next := pet.EffectInfo[:0] + for _, eff := range pet.EffectInfo { + if eff.Status == 2 { + if eff.LeftCount > 0 { + eff.LeftCount-- + } + if eff.LeftCount == 0 { + continue + } + } + next = append(next, eff) + } + pet.EffectInfo = next +} + func (f *FightC) battleLoop() { defer func() { if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值 @@ -69,26 +89,28 @@ func (f *FightC) battleLoop() { tt.Alive(false) //将所有属性变化失效掉 return true }) - if f.Info.Mode != info.BattleMode.PET_MELEE { //不是乱斗,传回血量 - for i := 0; i < len(ff.AllPet); i++ { - for j := 0; j < len(ff.Player.GetInfo().PetList); j++ { - if ff.Player.GetInfo().PetList[j].CatchTime == ff.AllPet[i].Info.CatchTime { - - if ff.UserID == f.WinnerId { - currentPet := ff.CurrentPet() - if currentPet != nil && currentPet.Info.CatchTime == ff.Player.GetInfo().PetList[j].CatchTime { - f.Winpet = &ff.Player.GetInfo().PetList[j] - } - - } - - ff.Player.GetInfo().PetList[j].Hp = utils.Min(ff.Player.GetInfo().PetList[j].MaxHp, ff.AllPet[i].Info.Hp) - ff.Player.GetInfo().PetList[j].SkillList = ff.AllPet[i].Info.SkillList - } - + for i := 0; i < len(ff.AllPet); i++ { + consumeLimitedPetEffects(&ff.AllPet[i].Info) + for j := 0; j < len(ff.Player.GetInfo().PetList); j++ { + if ff.Player.GetInfo().PetList[j].CatchTime != ff.AllPet[i].Info.CatchTime { + continue } - } + ff.Player.GetInfo().PetList[j].EffectInfo = ff.AllPet[i].Info.EffectInfo + if f.Info.Mode == info.BattleMode.PET_MELEE { + continue + } + + if ff.UserID == f.WinnerId { + currentPet := ff.CurrentPet() + if currentPet != nil && currentPet.Info.CatchTime == ff.Player.GetInfo().PetList[j].CatchTime { + f.Winpet = &ff.Player.GetInfo().PetList[j] + } + } + + ff.Player.GetInfo().PetList[j].Hp = utils.Min(ff.Player.GetInfo().PetList[j].MaxHp, ff.AllPet[i].Info.Hp) + ff.Player.GetInfo().PetList[j].SkillList = ff.AllPet[i].Info.SkillList + } } }) @@ -162,7 +184,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 +289,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 +311,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/fight/new.go b/logic/service/fight/new.go index 8b58c2b4b..b0ac43047 100644 --- a/logic/service/fight/new.go +++ b/logic/service/fight/new.go @@ -154,7 +154,7 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) { if ai.CanCapture > 0 { opp.CanCapture = ai.CanCapture } - opp.AttackValue.Prop = ai.Prop + ai.ApplyBattleProps(opp.AttackValue) } } } diff --git a/logic/service/fight/pvp/service.go b/logic/service/fight/pvp/service.go index 70c4d92a7..65a9988a4 100644 --- a/logic/service/fight/pvp/service.go +++ b/logic/service/fight/pvp/service.go @@ -28,6 +28,7 @@ const ( queueTTL = 12 * time.Second banPickTimeout = 45 * time.Second banPickStartCmd = 2461 + battleLevelCap = 100 ) type localQueueTicket struct { @@ -665,7 +666,11 @@ func resolveBattlePets(catchTimes []uint32, limit int) []model.PetInfo { if pet == nil || pet.Data.ID == 0 { continue } - result = append(result, pet.Data) + petInfo := pet.Data + if petInfo.Level > battleLevelCap { + petInfo.Level = battleLevelCap + } + result = append(result, petInfo) } return result } diff --git a/logic/service/fight/unified_state.go b/logic/service/fight/unified_state.go new file mode 100644 index 000000000..4ef43f2eb --- /dev/null +++ b/logic/service/fight/unified_state.go @@ -0,0 +1,113 @@ +package fight + +import ( + "blazing/logic/service/fight/info" + "blazing/logic/service/fight/input" +) + +// BuildFightStateStartEnvelope 构造开战阶段的统一状态包。 +func (f *FightC) BuildFightStateStartEnvelope() info.FightStateEnvelope { + return f.buildFightStateEnvelope(info.FightStatePhaseStart, 2504) +} + +// BuildFightStateSkillHurtEnvelope 构造技能结算阶段的统一状态包。 +func (f *FightC) BuildFightStateSkillHurtEnvelope() info.FightStateEnvelope { + envelope := f.buildFightStateEnvelope(info.FightStatePhaseSkillHurt, 2505) + envelope.SkillHurt = &info.FightSkillHurtState{ + Left: f.collectAttackValues(f.Our), + Right: f.collectAttackValues(f.Opp), + } + return envelope +} + +// BuildFightStateChangeEnvelope 构造切宠阶段的统一状态包。 +func (f *FightC) BuildFightStateChangeEnvelope(change info.ChangePetInfo) info.FightStateEnvelope { + envelope := f.buildFightStateEnvelope(info.FightStatePhaseChange, 2407) + envelope.Change = &change + return envelope +} + +// BuildFightStateOverEnvelope 构造结束阶段的统一状态包。 +func (f *FightC) BuildFightStateOverEnvelope() info.FightStateEnvelope { + envelope := f.buildFightStateEnvelope(info.FightStatePhaseOver, 2506) + envelope.Meta.WinnerID = f.FightOverInfo.WinnerId + envelope.Meta.Reason = f.FightOverInfo.Reason + return envelope +} + +// BuildFightStateLoadEnvelope 构造加载阶段的统一状态包。 +func (f *FightC) BuildFightStateLoadEnvelope(userID, percent uint32) info.FightStateEnvelope { + envelope := f.buildFightStateEnvelope(info.FightStatePhaseLoad, 2441) + envelope.Load = &info.FightLoadState{ + UserID: userID, + Percent: percent, + } + return envelope +} + +// BuildFightStateChatEnvelope 构造聊天阶段的统一状态包。 +func (f *FightC) BuildFightStateChatEnvelope(senderID uint32, senderNickname, message string) info.FightStateEnvelope { + envelope := f.buildFightStateEnvelope(info.FightStatePhaseChat, 50002) + envelope.Chat = &info.FightChatState{ + SenderID: senderID, + SenderNickname: senderNickname, + Message: message, + } + return envelope +} + +// buildFightStateEnvelope 组装左右两侧通用快照和元数据。 +func (f *FightC) buildFightStateEnvelope(phase info.FightStatePhase, legacyCmd uint32) info.FightStateEnvelope { + if f == nil { + return info.FightStateEnvelope{Phase: phase} + } + return info.FightStateEnvelope{ + Phase: phase, + Left: snapshotFighterStates(SideOur, f.Our), + Right: snapshotFighterStates(SideOpp, f.Opp), + Meta: info.FightStateMeta{ + Round: uint32(f.Round), + WinnerID: f.FightOverInfo.WinnerId, + Reason: f.FightOverInfo.Reason, + LegacyCmd: legacyCmd, + }, + } +} + +// snapshotFighterStates 把指定侧的战斗位数组转成统一 fighter 快照。 +func snapshotFighterStates(side int, fighters []*input.Input) []info.FighterState { + states := make([]info.FighterState, 0, len(fighters)) + for position, fighter := range fighters { + if fighter == nil { + continue + } + state := info.FighterState{ + Side: side, + Position: position, + } + if fighter.Player != nil && fighter.Player.GetInfo() != nil { + state.UserID = fighter.Player.GetInfo().UserID + } + if fighter.AttackValue != nil { + state.Status = fighter.AttackValue.Status + state.Prop = fighter.AttackValue.Prop + } + currentPet := fighter.CurrentPet() + if currentPet == nil { + states = append(states, state) + continue + } + state.ControllerUserID = currentPet.ControllerUserID + state.PetID = currentPet.Info.ID + state.CatchTime = currentPet.Info.CatchTime + state.Name = currentPet.Info.Name + state.HP = currentPet.Info.Hp + state.MaxHP = currentPet.Info.MaxHp + state.Level = currentPet.Info.Level + if len(currentPet.Info.SkillList) > 0 { + state.Skills = append(state.Skills, currentPet.Info.SkillList...) + } + states = append(states, state) + } + return states +} diff --git a/logic/service/fight/unified_test.go b/logic/service/fight/unified_test.go new file mode 100644 index 000000000..dfd03d1fd --- /dev/null +++ b/logic/service/fight/unified_test.go @@ -0,0 +1,113 @@ +package fight + +import ( + "testing" + + "blazing/common/socket/errorcode" + "blazing/logic/service/common" + fightinfo "blazing/logic/service/fight/info" + "blazing/logic/service/fight/input" + spaceinfo "blazing/logic/service/space/info" + "blazing/modules/player/model" +) + +type stubPlayer struct { + info model.PlayerInfo +} + +func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {} +func (*stubPlayer) GetPlayerCaptureContext() *fightinfo.PlayerCaptureContext { return nil } +func (*stubPlayer) Roll(int, int) (bool, float64, float64) { return false, 0, 0 } +func (*stubPlayer) Getfightinfo() fightinfo.Fightinfo { return fightinfo.Fightinfo{} } +func (*stubPlayer) ItemAdd(int64, int64) bool { return false } +func (p *stubPlayer) GetInfo() *model.PlayerInfo { return &p.info } +func (*stubPlayer) InvitePlayer(common.PlayerI) {} +func (*stubPlayer) SetFightC(common.FightI) {} +func (*stubPlayer) QuitFight() {} +func (*stubPlayer) MessWin(bool) {} +func (*stubPlayer) CanFight() errorcode.ErrorCode { return 0 } +func (*stubPlayer) SendPackCmd(uint32, any) {} +func (*stubPlayer) GetPetInfo(uint32) []model.PetInfo { return nil } + +func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) { + self := NewSkillActionEnvelope(1, 2, 0, SkillTargetSelf, 0) + if got := self.EncodedTargetIndex(); got != EncodeTargetIndex(2, false) { + t.Fatalf("expected self target to encode actor slot, got %d", got) + } + + ally := NewSkillActionEnvelope(1, 0, 1, SkillTargetAlly, 0) + if got := ally.EncodedTargetIndex(); got != EncodeTargetIndex(1, false) { + t.Fatalf("expected ally target to keep friendly slot, got %d", got) + } + + fallbackSelf := NewSkillActionEnvelope(1, 3, 0, 9, 3) + if got := fallbackSelf.EncodedTargetIndex(); got != EncodeTargetIndex(3, false) { + t.Fatalf("expected atkType=3 to fall back to self target, got %d", got) + } + + opponent := NewSkillActionEnvelope(1, 0, 2, SkillTargetOpponent, 0) + if got := opponent.EncodedTargetIndex(); got != EncodeTargetIndex(2, true) { + t.Fatalf("expected opponent target to stay on opposite side, got %d", got) + } +} + +func TestBuildFightStateStartEnvelope(t *testing.T) { + ourPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 1001}} + oppPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 2002}} + + our := input.NewInput(nil, ourPlayer) + our.InitAttackValue() + our.AttackValue.Prop[0] = 2 + our.AttackValue.Status[1] = 1 + ourPet := fightinfo.CreateBattlePetEntity(model.PetInfo{ + ID: 11, + Name: "Alpha", + Level: 20, + Hp: 88, + MaxHp: 100, + CatchTime: 101, + SkillList: []model.SkillInfo{{ID: 300, PP: 10}}, + }) + ourPet.BindController(ourPlayer.info.UserID) + our.SetCurPetAt(0, ourPet) + + opp := input.NewInput(nil, oppPlayer) + opp.InitAttackValue() + oppPet := fightinfo.CreateBattlePetEntity(model.PetInfo{ + ID: 22, + Name: "Beta", + Level: 21, + Hp: 77, + MaxHp: 110, + CatchTime: 202, + SkillList: []model.SkillInfo{{ID: 400, PP: 5}}, + }) + oppPet.BindController(oppPlayer.info.UserID) + opp.SetCurPetAt(0, oppPet) + + fc := &FightC{ + Our: []*input.Input{our}, + Opp: []*input.Input{opp}, + } + fc.Round = 7 + + envelope := fc.BuildFightStateStartEnvelope() + if envelope.Phase != fightinfo.FightStatePhaseStart { + t.Fatalf("expected start phase, got %s", envelope.Phase) + } + if envelope.Meta.Round != 7 { + t.Fatalf("expected round 7, got %d", envelope.Meta.Round) + } + if len(envelope.Left) != 1 || len(envelope.Right) != 1 { + t.Fatalf("expected one fighter on each side, got left=%d right=%d", len(envelope.Left), len(envelope.Right)) + } + if envelope.Left[0].UserID != 1001 || envelope.Left[0].PetID != 11 { + t.Fatalf("unexpected left fighter snapshot: %+v", envelope.Left[0]) + } + if envelope.Left[0].Prop[0] != 2 || envelope.Left[0].Status[1] != 1 { + t.Fatalf("expected prop/status snapshot to be copied, got %+v %+v", envelope.Left[0].Prop, envelope.Left[0].Status) + } + if envelope.Right[0].UserID != 2002 || envelope.Right[0].CatchTime != 202 { + t.Fatalf("unexpected right fighter snapshot: %+v", envelope.Right[0]) + } +} diff --git a/logic/service/item/petuse.go b/logic/service/item/petuse.go index f8362adcd..79cc0c27a 100644 --- a/logic/service/item/petuse.go +++ b/logic/service/item/petuse.go @@ -2,6 +2,7 @@ package item import ( "blazing/common/data/xmlres" + "blazing/common/socket/errorcode" "blazing/common/utils" "blazing/modules/player/model" "strings" @@ -26,6 +27,11 @@ type SetHandler struct { Handler PetItemHandler } +var fallbackPetItemNewSeIdx = map[uint32]int{ + 300741: 1103, // 瞬杀能量珠 + 300854: 1103, // 瞬杀能量珠Ω +} + // PetItemHandlerRegistry 道具处理器注册器 type PetItemHandlerRegistry struct { exactHandlers map[uint32]PetItemHandler // 精确ID映射 @@ -111,6 +117,91 @@ func nvfunc(itemid uint32, onpet *model.PetInfo) bool { return true } +func resolvePetItemNewSeIdx(itemid uint32) (itemCfg xmlres.Item, newSeIdx int, ok bool) { + itemCfg, ok = xmlres.ItemsMAP[int(itemid)] + if ok && itemCfg.NewSeIdx != 0 { + return itemCfg, itemCfg.NewSeIdx, true + } + + if newSeIdx, ok = fallbackPetItemNewSeIdx[itemid]; ok { + return itemCfg, newSeIdx, true + } + + for idx, effectCfg := range xmlres.EffectMAP { + if effectCfg.ItemId == nil || gconv.Uint32(*effectCfg.ItemId) != itemid { + continue + } + return itemCfg, idx, true + } + + return itemCfg, 0, false +} + +func handleNewSeIdxPetItem(itemid uint32, onpet *model.PetInfo) errorcode.ErrorCode { + itemCfg, newSeIdx, ok := resolvePetItemNewSeIdx(itemid) + if ok && newSeIdx == 0 { + if itemCfg.MaxHPUp > 0 { + if !onpet.AddMaxHPUpEffect(itemid, itemCfg.MaxHPUp) { + return errorcode.ErrorCodes.ErrCannotInjectPillAgain + } + return 0 + } + return errorcode.ErrorCodes.ErrItemUnusable + } + if !ok { + return errorcode.ErrorCodes.ErrItemUnusable + } + + effectCfg, ok := xmlres.EffectMAP[newSeIdx] + if !ok { + return errorcode.ErrorCodes.ErrSystemError + } + + effectStatus := byte(gconv.Int(effectCfg.Stat)) + effectIdx := uint16(newSeIdx) + leftCount := 1 + if effectCfg.Times != nil && *effectCfg.Times != "" { + leftCount = gconv.Int(*effectCfg.Times) + if leftCount <= 0 { + leftCount = 1 + } + } + + limitedCount := 0 + for _, eff := range onpet.EffectInfo { + if eff.Idx == effectIdx { + return errorcode.ErrorCodes.ErrCannotInjectPillAgain + } + if eff.Status == 2 { + limitedCount++ + } + } + if effectStatus == 2 && limitedCount >= 2 { + return errorcode.ErrorCodes.ErrTooManyEnergyOrbs + } + + onpet.EffectInfo = append(onpet.EffectInfo, model.PetEffectInfo{ + ItemID: itemid, + Idx: effectIdx, + Status: effectStatus, + LeftCount: byte(leftCount), + EID: uint16(gconv.Int(effectCfg.Eid)), + Args: effectCfg.ArgsS, + }) + return 0 +} + +func (r *PetItemHandlerRegistry) Handle(itemID uint32, onpet *model.PetInfo) errorcode.ErrorCode { + handler := r.GetHandler(itemID) + if handler != nil { + if handler(itemID, onpet) { + return 0 + } + return errorcode.ErrorCodes.ErrItemUnusable + } + return handleNewSeIdxPetItem(itemID, onpet) +} + // -------------------------- 6. 初始化注册器(注册所有处理器) -------------------------- func init() { 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/base.go b/logic/service/player/base.go index f74a82233..abb577e7f 100644 --- a/logic/service/player/base.go +++ b/logic/service/player/base.go @@ -44,6 +44,27 @@ func (p *baseplayer) GetPetInfo(limitlevel uint32) []model.PetInfo { } return ret } + +func (p *baseplayer) AddBattleProp(index int, level int8) { + if p == nil || index < 0 || index >= len(p.Prop) || level == 0 { + return + } + + p.Prop[index] += level + if p.Prop[index] > 6 { + p.Prop[index] = 6 + } + if p.Prop[index] < -6 { + p.Prop[index] = -6 + } +} + +func (p *baseplayer) ApplyBattleProps(target *model.AttackValue) { + if p == nil || target == nil { + return + } + target.Prop = p.Prop +} func (f *baseplayer) InvitePlayer(ff common.PlayerI) { } diff --git a/logic/service/player/pack.go b/logic/service/player/pack.go index 63c8dfe19..661cddeec 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" @@ -22,6 +23,11 @@ import ( "github.com/panjf2000/gnet/v2" ) +const ( + minPacketLen = 17 + maxPacketLen = 10 * 1024 +) + // getUnderlyingValue 递归解析reflect.Value,解包指针、interface{}到底层具体类型 func getUnderlyingValue(val reflect.Value) (reflect.Value, error) { for { @@ -47,6 +53,44 @@ func getUnderlyingValue(val reflect.Value) (reflect.Value, error) { } } +func setFieldByIndex(root reflect.Value, index []int, value reflect.Value) bool { + current := root + for pos, idx := range index { + if current.Kind() == reflect.Ptr { + if current.IsNil() { + current.Set(reflect.New(current.Type().Elem())) + } + current = current.Elem() + } + + if current.Kind() != reflect.Struct || idx < 0 || idx >= current.NumField() { + return false + } + + field := current.Field(idx) + if pos == len(index)-1 { + if !field.CanSet() { + return false + } + if value.Type().AssignableTo(field.Type()) { + field.Set(value) + return true + } + if field.Kind() == reflect.Ptr && value.Type().AssignableTo(field.Type().Elem()) { + ptr := reflect.New(field.Type().Elem()) + ptr.Elem().Set(value) + field.Set(ptr) + return true + } + return false + } + + current = field + } + + return false +} + // XORDecryptU 原地执行异或解密,避免额外分配和拷贝。 func XORDecryptU(encryptedData []byte, key uint32) []byte { if len(encryptedData) == 0 { @@ -102,6 +146,16 @@ func putPacketData(buf []byte) { } func (h *ClientData) PushEvent(v []byte, submit func(task func()) error) { + if h == nil || h.IsClosed() { + return + } + if len(v) < minPacketLen || len(v) > maxPacketLen { + return + } + if binary.BigEndian.Uint32(v[0:4]) != uint32(len(v)) { + return + } + var header common.TomeeHeader header.Len = binary.BigEndian.Uint32(v[0:4]) header.CMD = binary.BigEndian.Uint32(v[5:9]) @@ -111,9 +165,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) + } } // 重写 @@ -168,45 +231,38 @@ func (h *ClientData) OnEvent(data common.TomeeHeader) { return //TODO 待实现cmd未注册 } - params := []reflect.Value{} + var ptrValue reflect.Value + if cmdlister.NewReqValue != nil { + ptrValue = cmdlister.NewReqValue() + } else { + ptrValue = reflect.New(cmdlister.Req) + } - //funct := cmdlister.Type().NumIn() - - // 如果需要可设置的变量(用于修改值),创建指针并解引用 - ptrValue := reflect.New(cmdlister.Req) - - // fmt.Println(tt1) if data.Res != nil { - tt1 := ptrValue.Elem().Addr().Interface() - err := struc.Unpack(bytes.NewBuffer(data.Res), tt1) + err := struc.Unpack(bytes.NewBuffer(data.Res), ptrValue.Interface()) if err != nil { - cool.Logger.Error(context.Background(), data.UserID, data.CMD, "解包失败,", err, hex.EncodeToString(data.Res)) - //fmt.Println(data.UserID, data.CMD, "解包失败,", hex.EncodeToString(data.Data)) data.Result = uint32(errorcode.ErrorCodes.ErrSystemProcessingError) h.SendPack(data.Pack(nil)) return } } - ptrValue1 := ptrValue.Elem().Addr() - // 设置 Name 字段 - nameField := ptrValue.Elem().Field(0) //首个为header - nameField.Set(reflect.ValueOf(data)) - - if data.CMD > 1001 { //if cmdlister.Type().In(1) == reflect.TypeOf(&Player{}) { - //t := GetPlayer(c, data.UserID) - - // fmt.Println(data.CMD, "接收 变量的地址 ", &t.Info, t.Info.UserID) - - params = append(params, ptrValue1, reflect.ValueOf(h.Player)) - } else { - - params = append(params, ptrValue1, reflect.ValueOf(h.Conn)) + if !setFieldByIndex(ptrValue.Elem(), cmdlister.HeaderFieldIndex, reflect.ValueOf(data)) { + cool.Logger.Warning(context.Background(), data.UserID, data.CMD, "设置请求头失败") + return } - ret := cmdlister.Func.Call(params) + var params [2]reflect.Value + params[0] = ptrValue + if cmdlister.UseConn { + params[1] = reflect.ValueOf(h.Conn) + } else { + params[1] = reflect.ValueOf(h.Player) + } + + ret := cmdlister.Func.Call(params[:]) if len(ret) <= 0 { //如果判断没有参数,那就说明这个包没有返回参数 return @@ -235,12 +291,34 @@ func (h *ClientData) OnEvent(data common.TomeeHeader) { } type ClientData struct { - IsCrossDomain sync.Once //是否跨域过 - Player *Player //客户实体 - ERROR_CONNUT int - Wsmsg *WsCodec - Conn gnet.Conn - LF *lockfree.Lockfree[common.TomeeHeader] + Player *Player //客户实体 + ERROR_CONNUT int + Wsmsg *WsCodec + Conn gnet.Conn + LF *lockfree.Lockfree[common.TomeeHeader] + closed int32 + crossDomainChecked uint32 +} + +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) IsCrossDomainChecked() bool { + return atomic.LoadUint32(&p.crossDomainChecked) == 1 +} + +func (p *ClientData) MarkCrossDomainChecked() { + atomic.StoreUint32(&p.crossDomainChecked, 1) } 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/player/wscodec.go b/logic/service/player/wscodec.go index 5f83b2165..43b042cf1 100644 --- a/logic/service/player/wscodec.go +++ b/logic/service/player/wscodec.go @@ -2,6 +2,7 @@ package player import ( "bytes" + "encoding/binary" "errors" "io" @@ -11,12 +12,18 @@ import ( "github.com/panjf2000/gnet/v2/pkg/logging" ) +const ( + minTCPPacketLen = 17 + maxTCPPacketLen = 10 * 1024 + tomeeVersion = 49 +) + type WsCodec struct { - Tcp bool - Upgraded bool // 链接是否升级 - Buf bytes.Buffer // 从实际socket中读取到的数据缓存 - wsMsgBuf wsMessageBuf // ws 消息缓存 - //Isinitws bool + Tcp bool + Upgraded bool // 链接是否升级 + Buf bytes.Buffer // 从实际socket中读取到的数据缓存 + wsMsgBuf wsMessageBuf // ws 消息缓存 + bufferedInbound int // 已镜像到 Buf 中的 inbound 字节数 } type wsMessageBuf struct { @@ -24,92 +31,115 @@ type wsMessageBuf struct { cachedBuf bytes.Buffer } +type UpgradeState uint8 + +const ( + UpgradeNeedMoreData UpgradeState = iota + UpgradeUseTCP + UpgradeUseWS +) + type readWrite struct { io.Reader io.Writer } -func CompareLeftBytes(array1, array2 []byte, leftBytesCount int) bool { - // 检查切片长度是否足够比较左边的字节 - if len(array1) < leftBytesCount || len(array2) < leftBytesCount { - return false - } - - // 提取左边的字节切片 - left1 := array1[:leftBytesCount] - left2 := array2[:leftBytesCount] - - // 比较左边的字节切片 - for i := 0; i < leftBytesCount; i++ { - if left1[i] != left2[i] { - return false - } - } - - return true -} -func (w *WsCodec) Upgrade(c gnet.Conn) (ok bool, action gnet.Action) { +func (w *WsCodec) Upgrade(c gnet.Conn) (state UpgradeState, action gnet.Action) { if w.Upgraded { - ok = true + state = UpgradeUseWS return } - if w.Tcp { - ok = false + state = UpgradeUseTCP return } - buf := &w.Buf - if CompareLeftBytes(buf.Bytes(), []byte{0, 0}, 2) { - w.Tcp = true - return - } - tmpReader := bytes.NewReader(buf.Bytes()) - oldLen := tmpReader.Len() - //logging.Infof("do Upgrade") + buf := w.Buf.Bytes() + if looksLikeTCPPacket(buf) { + w.SwitchToTCP() + state = UpgradeUseTCP + return + } + if len(buf) == 0 { + state = UpgradeNeedMoreData + return + } + + tmpReader := bytes.NewReader(buf) + oldLen := tmpReader.Len() hs, err := ws.Upgrade(readWrite{tmpReader, c}) skipN := oldLen - tmpReader.Len() if err != nil { - if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) { //数据不完整,不跳过 buf 中的 skipN 字节(此时 buf 中存放的仅是部分 "handshake data" bytes),下次再尝试读取 + if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) { + state = UpgradeNeedMoreData return } - buf.Next(skipN) + w.Buf.Next(skipN) logging.Errorf("conn[%v] [err=%v]", c.RemoteAddr().String(), err.Error()) action = gnet.Close - //ok = true - //w.Tcp = true return } - buf.Next(skipN) - logging.Infof("conn[%v] upgrade websocket protocol! Handshake: %v", c.RemoteAddr().String(), hs) - ok = true + w.Buf.Next(skipN) + logging.Infof("conn[%v] upgrade websocket protocol! Handshake: %v", c.RemoteAddr().String(), hs) w.Upgraded = true + state = UpgradeUseWS return } + +func looksLikeTCPPacket(buf []byte) bool { + if len(buf) < 4 { + return false + } + packetLen := binary.BigEndian.Uint32(buf[:4]) + if packetLen < minTCPPacketLen || packetLen > maxTCPPacketLen { + return false + } + if len(buf) >= 5 && buf[4] != tomeeVersion { + return false + } + return true +} + func (w *WsCodec) ReadBufferBytes(c gnet.Conn) (gnet.Action, int) { size := c.InboundBuffered() - //buf := make([]byte, size) + if size < w.bufferedInbound { + w.bufferedInbound = 0 + } + if size == w.bufferedInbound { + return gnet.None, size + } + read, err := c.Peek(size) if err != nil { logging.Errorf("read err! %v", err) return gnet.Close, 0 } - // if read < size { - // logging.Errorf("read bytes len err! size: %d read: %d", size, read) - // return gnet.Close - // } - w.Buf.Write(read) + w.Buf.Write(read[w.bufferedInbound:]) + w.bufferedInbound = size return gnet.None, size } + +func (w *WsCodec) ResetInboundMirror() { + w.bufferedInbound = 0 +} + +func (w *WsCodec) SwitchToTCP() { + w.Tcp = true + w.Upgraded = false + w.bufferedInbound = 0 + w.Buf.Reset() + w.wsMsgBuf.curHeader = nil + w.wsMsgBuf.cachedBuf.Reset() +} + func (w *WsCodec) Decode(c gnet.Conn) (outs []wsutil.Message, err error) { - // fmt.Println("do Decode") messages, err := w.readWsMessages() if err != nil { logging.Errorf("Error reading message! %v", err) return nil, err } - if len(messages) <= 0 { //没有读到完整数据 不处理 + if len(messages) <= 0 { return } for _, message := range messages { @@ -131,9 +161,8 @@ func (w *WsCodec) readWsMessages() (messages []wsutil.Message, err error) { msgBuf := &w.wsMsgBuf in := &w.Buf for { - // 从 in 中读出 header,并将 header bytes 写入 msgBuf.cachedBuf if msgBuf.curHeader == nil { - if in.Len() < ws.MinHeaderSize { //头长度至少是2 + if in.Len() < ws.MinHeaderSize { return } var head ws.Header @@ -142,13 +171,13 @@ func (w *WsCodec) readWsMessages() (messages []wsutil.Message, err error) { if err != nil { return messages, err } - } else { //有可能不完整,构建新的 reader 读取 head,读取成功才实际对 in 进行读操作 + } else { tmpReader := bytes.NewReader(in.Bytes()) oldLen := tmpReader.Len() head, err = ws.ReadHeader(tmpReader) skipN := oldLen - tmpReader.Len() if err != nil { - if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) { //数据不完整 + if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) { return messages, nil } in.Next(skipN) @@ -163,21 +192,19 @@ func (w *WsCodec) readWsMessages() (messages []wsutil.Message, err error) { return nil, err } } - dataLen := (int)(msgBuf.curHeader.Length) - // 从 in 中读出 data,并将 data bytes 写入 msgBuf.cachedBuf - if dataLen > 0 { - if in.Len() < dataLen { //数据不完整 + dataLen := int(msgBuf.curHeader.Length) + if dataLen > 0 { + if in.Len() < dataLen { logging.Infof("incomplete data") return } - _, err = io.CopyN(&msgBuf.cachedBuf, in, int64(dataLen)) if err != nil { return } } - if msgBuf.curHeader.Fin { //当前 header 已经是一个完整消息 + if msgBuf.curHeader.Fin { messages, err = wsutil.ReadClientMessage(&msgBuf.cachedBuf, messages) if err != nil { return nil, err diff --git a/logic/service/space/space.go b/logic/service/space/space.go index 1381a7232..2b6cc2ca1 100644 --- a/logic/service/space/space.go +++ b/logic/service/space/space.go @@ -45,6 +45,7 @@ type Space struct { IsTime bool DropItemIds []uint32 PitS *csmap.CsMap[int, []model.MapPit] + MapNodeS *csmap.CsMap[uint32, *model.MapNode] } func NewSpace() *Space { @@ -52,6 +53,7 @@ func NewSpace() *Space { ret := &Space{ User: csmap.New[uint32, common.PlayerI](), UserInfo: csmap.New[uint32, info.SimpleInfo](), + MapNodeS: csmap.New[uint32, *model.MapNode](), } return ret @@ -185,12 +187,20 @@ func (ret *Space) init() { } ret.MapBossSInfo = info.MapModelBroadcastInfo{} ret.MapBossSInfo.INFO = make([]info.MapModelBroadcastEntry, 0) + + mapNodes := service.NewMapNodeService().GetData(ret.ID) + for i := range mapNodes { + ret.MapNodeS.Store(mapNodes[i].NodeID, &mapNodes[i]) + } if len(r.WeatherType) > 1 { ret.WeatherType = r.WeatherType cool.Cron.CustomFunc(ret, ret.GenWer) } - for _, v := range service.NewMapNodeService().GetDataB(ret.ID) { + for _, v := range mapNodes { + if v.IsBroadcast == 0 { + continue + } r := service.NewMapmodelService().GetDataByModelId(v.IsBroadcast) if r == nil { @@ -220,15 +230,29 @@ 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 - +func (p *Space) GetMatchedMapNode(nodeID uint32) *model.MapNode { + if p == nil || p.MapNodeS == nil { + return nil } + + mapNode, ok := p.MapNodeS.Load(nodeID) + if !ok || mapNode == nil || mapNode.Event == nil || !p.IsMatch(*mapNode.Event) { + return nil + } + + return mapNode +} + +func (p *Space) IsMatch(t model.Event) bool { + 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 +260,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/controller/admin/sign.go b/modules/config/controller/admin/sign.go new file mode 100644 index 000000000..03d89ed05 --- /dev/null +++ b/modules/config/controller/admin/sign.go @@ -0,0 +1,20 @@ +package admin + +import ( + "blazing/cool" + "blazing/modules/config/service" +) + +type SignController struct { + *cool.Controller +} + +func init() { + cool.RegisterController(&SignController{ + &cool.Controller{ + Prefix: "/admin/config/sign", + Api: []string{"Add", "Delete", "Update", "Info", "List", "Page"}, + Service: service.NewSignInService(), + }, + }) +} diff --git a/modules/config/model/boss_pet.go b/modules/config/model/boss_pet.go index 95a27ee54..270a63d15 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 ( @@ -24,25 +28,143 @@ type BossConfig struct { Rule []uint32 `gorm:"type:jsonb; ;comment:'战胜规则'" json:"rule"` } -// TableName 指定BossConfig对应的数据库表名 -func (*BossConfig) TableName() string { - return TableNameBossConfig +// BossHookSkillContext 为脚本暴露当前精灵技能可用信息。 +type BossHookSkillContext struct { + SkillID uint32 `json:"skill_id"` + PP uint32 `json:"pp"` + CanUse bool `json:"can_use"` } -// GroupName 指定表所属的分组(保持和怪物刷新表一致) -func (*BossConfig) GroupName() string { - return "default" +// BossHookPetContext 为脚本暴露战斗中双方精灵简要信息。 +type BossHookPetContext struct { + PetID uint32 `json:"pet_id"` + CatchTime uint32 `json:"catch_time"` + Hp uint32 `json:"hp"` + MaxHp uint32 `json:"max_hp"` } -// NewBossConfig 创建一个新的BossConfig实例(初始化通用Model字段+所有默认值) - -func NewBossConfig() *BossConfig { - return &BossConfig{ - Model: cool.NewModel(), - } +// BossHookAttackContext 参考 AttackValue,为脚本暴露关键战斗面板/结果字段。 +type BossHookAttackContext struct { + SkillID uint32 `json:"skill_id"` + AttackTime uint32 `json:"attack_time"` + IsCritical uint32 `json:"is_critical"` + LostHp uint32 `json:"lost_hp"` + GainHp int32 `json:"gain_hp"` + RemainHp int32 `json:"remain_hp"` + MaxHp uint32 `json:"max_hp"` + State uint32 `json:"state"` + Offensive float32 `json:"offensive"` + Status []int8 `json:"status"` + Prop []int8 `json:"prop"` } -// init 程序启动时自动创建/同步boss_config表结构 +// BossHookActionContext 为 boss 脚本提供可读写的出手上下文。 +type BossHookActionContext struct { + HookAction bool `json:"hookaction"` // effect 链原始 HookAction 判定 + Round uint32 `json:"round"` // 当前回合数 + IsFirst bool `json:"is_first"` // 是否先手 + Our *BossHookPetContext `json:"our"` // 我方当前精灵 + Opp *BossHookPetContext `json:"opp"` // 对方当前精灵 + Skills []BossHookSkillContext `json:"skills"` // 我方技能 + OurAttack *BossHookAttackContext `json:"our_attack"` // 我方AttackValue快照 + OppAttack *BossHookAttackContext `json:"opp_attack"` // 对方AttackValue快照 + Action string `json:"action"` // auto/skill/switch + SkillID uint32 `json:"skill_id"` // action=skill + CatchTime uint32 `json:"catch_time"` // action=switch + + UseSkillFn func(skillID uint32) `json:"-"` + SwitchPetFn func(catchTime uint32) `json:"-"` +} + +func (*BossConfig) TableName() string { return TableNameBossConfig } +func (*BossConfig) GroupName() string { return "default" } +func NewBossConfig() *BossConfig { return &BossConfig{Model: cool.NewModel()} } + func init() { cool.CreateTable(&BossConfig{}) } + +// RunHookActionScript 执行 BOSS 脚本 hookAction。 +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() + vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) + bindBossScriptFunctions(vm, hookAction) + + 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) + } + + if goja.IsUndefined(result) || goja.IsNull(result) { + return defaultHookActionResult(hookAction), nil + } + return result.ToBoolean(), nil +} + +func bindBossScriptFunctions(vm *goja.Runtime, hookAction any) { + ctx, ok := hookAction.(*BossHookActionContext) + if !ok || ctx == nil { + return + } + + _ = vm.Set("useSkill", func(call goja.FunctionCall) goja.Value { + if ctx.UseSkillFn == nil || len(call.Arguments) == 0 { + return goja.Undefined() + } + skillID := call.Arguments[0].ToInteger() + if skillID < 0 { + return goja.Undefined() + } + ctx.UseSkillFn(uint32(skillID)) + return goja.Undefined() + }) + + _ = vm.Set("switchPet", func(call goja.FunctionCall) goja.Value { + if ctx.SwitchPetFn == nil || len(call.Arguments) == 0 { + return goja.Undefined() + } + catchTime := call.Arguments[0].ToInteger() + if catchTime < 0 { + return goja.Undefined() + } + ctx.SwitchPetFn(uint32(catchTime)) + return goja.Undefined() + }) +} + +func defaultHookActionResult(hookAction any) bool { + if ctx, ok := hookAction.(*BossHookActionContext); ok { + return ctx.HookAction + } + if val, ok := hookAction.(bool); ok { + return val + } + return true +} diff --git a/modules/config/model/boss_pet_test.go b/modules/config/model/boss_pet_test.go new file mode 100644 index 000000000..f33fb5cca --- /dev/null +++ b/modules/config/model/boss_pet_test.go @@ -0,0 +1,87 @@ +package model + +import "testing" + +func TestBossConfigRunHookActionScript(t *testing.T) { + boss := &BossConfig{ + Script: ` + function hookAction(hookaction) { + return hookaction.hookaction === true; + } + `, + } + + ctx := &BossHookActionContext{HookAction: true} + ok, err := boss.RunHookActionScript(ctx) + if err != nil { + t.Fatalf("RunHookActionScript returned error: %v", err) + } + if !ok { + t.Fatalf("RunHookActionScript = false, want true") + } +} + +func TestBossConfigRunHookActionScriptCallUseSkillFn(t *testing.T) { + boss := &BossConfig{ + Script: ` + function hookAction(hookaction) { + if (hookaction.round >= 2) { + useSkill(5001); + } + return true; + } + `, + } + + ctx := &BossHookActionContext{ + HookAction: true, + Round: 2, + Action: "auto", + } + ctx.UseSkillFn = func(skillID uint32) { + ctx.Action = "skill" + ctx.SkillID = skillID + } + + ok, err := boss.RunHookActionScript(ctx) + if err != nil { + t.Fatalf("RunHookActionScript returned error: %v", err) + } + if !ok { + t.Fatalf("RunHookActionScript = false, want true") + } + if ctx.Action != "skill" || ctx.SkillID != 5001 { + t.Fatalf("useSkill not applied, got action=%q skill_id=%d", ctx.Action, ctx.SkillID) + } +} + +func TestBossConfigRunHookActionScriptCallSwitchPetFn(t *testing.T) { + boss := &BossConfig{ + Script: ` + function hookAction(hookaction) { + switchPet(3); + return true; + } + `, + } + + ctx := &BossHookActionContext{ + HookAction: true, + Action: "auto", + } + ctx.SwitchPetFn = func(catchTime uint32) { + ctx.Action = "switch" + ctx.CatchTime = catchTime + } + + ok, err := boss.RunHookActionScript(ctx) + if err != nil { + t.Fatalf("RunHookActionScript returned error: %v", err) + } + if !ok { + t.Fatalf("RunHookActionScript = false, want true") + } + if ctx.Action != "switch" || ctx.CatchTime != 3 { + t.Fatalf("switchPet not applied, got action=%q catch_time=%d", ctx.Action, ctx.CatchTime) + } +} diff --git a/modules/config/model/sign.go b/modules/config/model/sign.go new file mode 100644 index 000000000..f708da02c --- /dev/null +++ b/modules/config/model/sign.go @@ -0,0 +1,34 @@ +package model + +import "blazing/cool" + +const TableNameSignIn = "config_sign_in" + +const ( + SignTypeTotal uint32 = 1 + SignTypeContinuous uint32 = 2 +) + +// SignIn 签到阶段配置表。 +type SignIn struct { + *BaseConfig + SignType uint32 `gorm:"not null;default:1;uniqueIndex:idx_sign_type_stage;comment:'签到类别(1-累计 2-连续)'" json:"sign_type"` + StageDays uint32 `gorm:"not null;default:1;uniqueIndex:idx_sign_type_stage;comment:'签到阶段天数(0/1/3/7/14/30)'" json:"stage_days"` + CdkID uint32 `gorm:"not null;uniqueIndex;comment:'绑定的CDK配置ID'" json:"cdk_id"` +} + +func (*SignIn) TableName() string { + return TableNameSignIn +} + +func (*SignIn) GroupName() string { + return "default" +} + +func NewSignIn() *SignIn { + return &SignIn{BaseConfig: NewBaseConfig()} +} + +func init() { + cool.CreateTable(&SignIn{}) +} diff --git a/modules/config/service/cdk.go b/modules/config/service/cdk.go index 9928f1d29..66465e219 100644 --- a/modules/config/service/cdk.go +++ b/modules/config/service/cdk.go @@ -8,14 +8,13 @@ import ( "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/grand" "github.com/google/uuid" -) // 1. 扩展字符集:数字+大小写字母+安全符号(避开URL/输入易冲突的符号,如/、?、&) +) const charsetWithSymbol = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz" func Generate16CharSecure() string { result := make([]byte, 16) for i := 0; i < 16; i++ { - result[i] = charsetWithSymbol[grand.N(0, len(charsetWithSymbol)-1)] } return string(result) @@ -38,22 +37,30 @@ func NewCdkService() *CdkService { }, } } + func (s *CdkService) Get(id string) *model.CDKConfig { var item *model.CDKConfig dbm_notenable(s.Model).Where("cdk_code", id).WhereNot("exchange_remain_count", 0).Scan(&item) - return item - } + +func (s *CdkService) GetByID(id uint32) *model.CDKConfig { + if id == 0 { + return nil + } + + var item *model.CDKConfig + dbm_notenable(s.Model).Where("id", id).Scan(&item) + return item +} + func (s *CdkService) All() []model.CDKConfig { var item []model.CDKConfig dbm_notenable(s.Model).WhereLT("exchange_remain_count", 0).Scan(&item) - return item - } -func (s *CdkService) Set(id string) bool { +func (s *CdkService) Set(id string) bool { res, err := cool.DBM(s.Model).Where("cdk_code", id).WhereNot("exchange_remain_count", 0).Decrement("exchange_remain_count", 1) if err != nil { return false @@ -62,7 +69,5 @@ func (s *CdkService) Set(id string) bool { if rows == 0 { return false } - return true - } diff --git a/modules/config/service/sign.go b/modules/config/service/sign.go new file mode 100644 index 000000000..c896f2f54 --- /dev/null +++ b/modules/config/service/sign.go @@ -0,0 +1,80 @@ +package service + +import ( + "blazing/cool" + "blazing/modules/config/model" + "context" + "fmt" + "sort" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gconv" +) + +var signStageDays = map[uint32]struct{}{ + 0: {}, + 1: {}, + 3: {}, + 7: {}, + 14: {}, + 30: {}, +} + +type SignInService struct { + *cool.Service +} + +func NewSignInService() *SignInService { + return &SignInService{ + &cool.Service{ + Model: model.NewSignIn(), + PageQueryOp: &cool.QueryOp{ + FieldEQ: []string{"sign_type", "stage_days", "cdk_id", "is_enable"}, + KeyWordField: []string{"remark"}, + }, + ListQueryOp: &cool.QueryOp{ + FieldEQ: []string{"sign_type", "stage_days", "cdk_id", "is_enable"}, + }, + }, + } +} + +func (s *SignInService) ModifyBefore(ctx context.Context, method string, param g.MapStrAny) (err error) { + if method == "Delete" { + return nil + } + + signType := gconv.Uint32(param["sign_type"]) + if signType != model.SignTypeTotal && signType != model.SignTypeContinuous { + return fmt.Errorf("签到类别非法,只支持1(累计)或2(连续)") + } + + stageDays := gconv.Uint32(param["stage_days"]) + if _, ok := signStageDays[stageDays]; !ok { + return fmt.Errorf("签到阶段仅支持0、1、3、7、14、30天") + } + + cdkID := gconv.Uint32(param["cdk_id"]) + if cdkID == 0 { + return fmt.Errorf("cdk_id不能为空") + } + if NewCdkService().GetByID(cdkID) == nil { + return fmt.Errorf("绑定的CDK不存在") + } + return nil +} + +func (s *SignInService) GetEnabled() []model.SignIn { + var items []model.SignIn + dbm_enable(s.Model).Scan(&items) + sort.Slice(items, func(i, j int) bool { + if items[i].SignType != items[j].SignType { + return items[i].SignType < items[j].SignType + } + if items[i].StageDays != items[j].StageDays { + return items[i].StageDays < items[j].StageDays + } + return items[i].CdkID < items[j].CdkID + }) + return items +} diff --git a/modules/player/controller/admin/sign.go b/modules/player/controller/admin/sign.go new file mode 100644 index 000000000..3b09cd65d --- /dev/null +++ b/modules/player/controller/admin/sign.go @@ -0,0 +1,36 @@ +package admin + +import ( + "blazing/cool" + "blazing/modules/player/service" + "context" + + "github.com/gogf/gf/v2/frame/g" +) + +type SignRecordController struct { + *cool.Controller +} + +func init() { + cool.RegisterController(&SignRecordController{ + &cool.Controller{ + Prefix: "/admin/game/signrecord", + Api: []string{"Delete", "Update", "Info", "List", "Page"}, + Service: service.NewSignService(0), + }, + }) +} + +type ResetAllReq struct { + g.Meta `path:"/resetAll" method:"POST"` + Authorization string `json:"Authorization" in:"header"` +} + +func (c *SignRecordController) ResetAll(ctx context.Context, req *ResetAllReq) (res *cool.BaseRes, err error) { + result, err := service.NewSignService(0).ResetAll() + if err != nil { + return cool.Fail(err.Error()), nil + } + return cool.Ok(result), nil +} diff --git a/modules/player/controller/app/sign.go b/modules/player/controller/app/sign.go new file mode 100644 index 000000000..7b0e3e801 --- /dev/null +++ b/modules/player/controller/app/sign.go @@ -0,0 +1,104 @@ +package app + +import ( + "blazing/cool" + configservice "blazing/modules/config/service" + playerservice "blazing/modules/player/service" + "context" + "fmt" + "strings" + + "github.com/deatil/go-cryptobin/cryptobin/crypto" + "github.com/gogf/gf/v2/frame/g" +) + +type SignController struct { + *cool.Controller +} + +func init() { + controller := &SignController{ + &cool.Controller{ + Prefix: "/seer/game/sign", + Api: []string{}, + Service: configservice.NewSignInService(), + }, + } + cool.RegisterController(controller) +} + +type SignStateReq struct { + g.Meta `path:"/state" method:"GET"` + UserID uint32 `json:"user_id" v:"required|min:1#用户ID不能为空|用户ID非法"` + Session string `json:"session" v:"required#session不能为空"` +} + +type SignClaimReq struct { + g.Meta `path:"/claim" method:"POST"` + UserID uint32 `json:"user_id" v:"required|min:1#用户ID不能为空|用户ID非法"` + Session string `json:"session" v:"required#session不能为空"` +} + +func (c *SignController) State(ctx context.Context, req *SignStateReq) (res *cool.BaseRes, err error) { + if err = g.Validator().Data(req).Run(ctx); err != nil { + return cool.Fail(err.Error()), nil + } + if err = validateGameSession(req.UserID, req.Session); err != nil { + return cool.Fail(err.Error()), nil + } + + state, err := playerservice.NewSignService(req.UserID).GetState() + if err != nil { + return cool.Fail(err.Error()), nil + } + return cool.Ok(state), nil +} + +func (c *SignController) Claim(ctx context.Context, req *SignClaimReq) (res *cool.BaseRes, err error) { + if err = g.Validator().Data(req).Run(ctx); err != nil { + return cool.Fail(err.Error()), nil + } + if err = validateGameSession(req.UserID, req.Session); err != nil { + return cool.Fail(err.Error()), nil + } + + result, err := playerservice.NewSignService(req.UserID).Claim() + if err != nil { + return cool.Fail(err.Error()), nil + } + return cool.Ok(result), nil +} + +func validateGameSession(userID uint32, session string) error { + if userID == 0 { + return fmt.Errorf("user_id不能为空") + } + session = strings.TrimSpace(session) + if session == "" { + return fmt.Errorf("session不能为空") + } + + cached, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", userID)) + if err != nil || cached.IsEmpty() { + return fmt.Errorf("session已过期,请重新登录") + } + + rawSession := session + decrypted := crypto. + FromBase64String(session). + SetKey("gfertf12dfertf12"). + SetIv("gfertf12dfertf12"). + Aes(). + CBC(). + PKCS7Padding(). + Decrypt(). + ToString() + if decrypted != "" { + rawSession = decrypted + } + + if rawSession != cached.String() { + return fmt.Errorf("session无效,请重新登录") + } + return nil +} diff --git a/modules/player/model/pet.go b/modules/player/model/pet.go index 6b9cb4d03..ef0b47fc5 100644 --- a/modules/player/model/pet.go +++ b/modules/player/model/pet.go @@ -383,16 +383,119 @@ func (pet *PetInfo) RnadEffect() { // 7 :繁殖加成 // 8 :体力提升加成 +const ( + maxHPUpEffectIdx uint16 = 60000 + maxHPUpEffectStatus byte = 8 + maxHPUpEffectEID uint16 = 26 + maxHPUpEffectCap = 20 + trainingEffectStatus byte = 5 + trainingAttrEffectIdx uint16 = 60001 + trainingPowerEffectIdx uint16 = 60002 + trainingAttrEffectEID uint16 = 247 + trainingPowerEffectEID uint16 = 239 +) + // 繁殖加成,体力提升加成 ,这里是防止和其他重复所以定义不同类别,但是实际上,能量珠那些事调用不同id的effect实现 // func (pet *PetInfo) GetEffect(ptype int) (int, *PetEffectInfo, bool) { return utils.FindWithIndex(pet.EffectInfo, func(item PetEffectInfo) bool { - return item.Status == 1 + return int(item.Status) == ptype }) } +func (pet *PetInfo) getEffectByStatusAndEID(status byte, eid uint16) (int, *PetEffectInfo, bool) { + return utils.FindWithIndex(pet.EffectInfo, func(item PetEffectInfo) bool { + return item.Status == status && item.EID == eid + }) +} + +func ensureEffectArgsLen(args []int, size int) []int { + if len(args) >= size { + return args + } + next := make([]int, size) + copy(next, args) + return next +} + +func (pet *PetInfo) addTrainingEffectDelta(idx uint16, eid uint16, argsLen int, argIndex int, value int) bool { + if pet == nil || value <= 0 || argIndex < 0 || argIndex >= argsLen { + return false + } + + if _, eff, ok := pet.getEffectByStatusAndEID(trainingEffectStatus, eid); ok { + if eff.Idx == 0 { + eff.Idx = idx + } + eff.Status = trainingEffectStatus + eff.EID = eid + eff.Args = ensureEffectArgsLen(eff.Args, argsLen) + eff.Args[argIndex] += value + return true + } + + args := make([]int, argsLen) + args[argIndex] = value + pet.EffectInfo = append(pet.EffectInfo, PetEffectInfo{ + Idx: idx, + Status: trainingEffectStatus, + EID: eid, + Args: args, + }) + return true +} + +func (pet *PetInfo) AddTrainingAttrBonus(attr int, value int) bool { + return pet.addTrainingEffectDelta(trainingAttrEffectIdx, trainingAttrEffectEID, 6, attr, value) +} + +func (pet *PetInfo) AddTrainingPowerBonus(value int) bool { + return pet.addTrainingEffectDelta(trainingPowerEffectIdx, trainingPowerEffectEID, 2, 0, value) +} + +func (pet *PetInfo) AddMaxHPUpEffect(itemID uint32, value int) bool { + if pet == nil || value <= 0 { + return false + } + + if _, eff, ok := pet.GetEffect(int(maxHPUpEffectStatus)); ok { + current := 0 + if len(eff.Args) >= 2 && eff.Args[0] == 0 && eff.Args[1] > 0 { + current = eff.Args[1] + } + if current >= maxHPUpEffectCap { + return false + } + + next := current + value + if next > maxHPUpEffectCap { + next = maxHPUpEffectCap + } + + eff.ItemID = itemID + eff.Idx = maxHPUpEffectIdx + eff.Status = maxHPUpEffectStatus + eff.EID = maxHPUpEffectEID + eff.Args = []int{0, next} + return next > current + } + + if value > maxHPUpEffectCap { + value = maxHPUpEffectCap + } + + pet.EffectInfo = append(pet.EffectInfo, PetEffectInfo{ + ItemID: itemID, + Idx: maxHPUpEffectIdx, + Status: maxHPUpEffectStatus, + EID: maxHPUpEffectEID, + Args: []int{0, value}, + }) + return true +} + func (pet *PetInfo) Downgrade(level uint32) { for pet.Level > uint32(level) { diff --git a/modules/player/model/sign.go b/modules/player/model/sign.go index 20dc281cb..406d94f44 100644 --- a/modules/player/model/sign.go +++ b/modules/player/model/sign.go @@ -16,9 +16,12 @@ type SignInRecord struct { PlayerID uint32 `gorm:"not null;index:idx_player_id;comment:'玩家ID'" json:"player_id"` SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'关联的签到活动ID(对应player_sign_in表的SignInID)'" json:"sign_in_id"` - IsCompleted bool `gorm:"not null;default:false;comment:'签到是否完成(0-未完成 1-已完成)'" json:"is_completed"` - //通过bitset来实现签到的进度记录 - SignInProgress []uint32 `gorm:"type:jsonb;not null;comment:'签到进度(状压实现,存储每日签到状态)'" json:"sign_in_progress"` + IsCompleted bool `gorm:"not null;default:false;comment:'签到是否完成(0-未完成 1-已完成)'" json:"is_completed"` + ContinuousDays uint32 `gorm:"not null;default:0;comment:'连续签到天数'" json:"continuous_days"` + TotalDays uint32 `gorm:"not null;default:0;comment:'累计签到天数'" json:"total_days"` + LastSignDate string `gorm:"type:varchar(10);not null;default:'';comment:'最近一次签到日期(YYYY-MM-DD)'" json:"last_sign_date"` + // 通过 bitset 记录每日签到状态,位索引从 0 开始,对应签到第 1 天。 + SignInProgress []uint32 `gorm:"type:jsonb;not null;default:'[]';comment:'签到进度(状压实现,存储每日签到状态)'" json:"sign_in_progress"` } // TableName 指定表名(遵循现有规范) diff --git a/modules/player/model/user_sign.go b/modules/player/model/user_sign.go index afbdce2f2..4b1d59360 100644 --- a/modules/player/model/user_sign.go +++ b/modules/player/model/user_sign.go @@ -1,40 +1,14 @@ package model -import ( - "blazing/cool" -) +import configmodel "blazing/modules/config/model" -// 表名常量(遵循现有命名规范:小写+下划线) -const TableNameSignIn = "config_sign_in" +// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。 +const TableNameSignIn = configmodel.TableNameSignIn -// SignIn 签到记录表 -// 核心字段:签到完成状态、状压签到进度、签到奖励脚本 -type SignIn struct { - *cool.Model // 嵌入基础Model(包含主键、创建/更新时间等通用字段) - SignInID uint32 `gorm:"not null;index:idx_sign_in_id;comment:'签到活动ID'" json:"sign_in_id"` - Status uint32 `gorm:"not null;default:0;comment:'签到状态(0-未完成 1-已完成)'" json:"status"` - //传入用户名,签到天数,给予奖励,这个搭配里程碑表实现 - RewardScript string `gorm:"type:varchar(512);default:'';comment:'签到奖励脚本(执行奖励发放的脚本内容)'" json:"reward_script"` -} - -// TableName 指定表名(遵循现有规范) -func (*SignIn) TableName() string { - return TableNameSignIn -} - -// GroupName 指定表分组(默认分组,与现有Item表/精灵特效表一致) -func (*SignIn) GroupName() string { - return "default" -} - -// NewSignIn 创建签到记录表实例(初始化基础Model) -func NewSignIn() *SignIn { - return &SignIn{ - Model: cool.NewModel(), - } -} - -// init 程序启动时自动创建表(与现有PlayerPetSpecialEffect表的初始化逻辑一致) -func init() { - cool.CreateTable(&SignIn{}) +// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。 +type SignIn = configmodel.SignIn + +// Deprecated: 签到配置已迁移到 modules/config/model/sign.go。 +func NewSignIn() *configmodel.SignIn { + return configmodel.NewSignIn() } diff --git a/modules/player/service/cdk_reward.go b/modules/player/service/cdk_reward.go new file mode 100644 index 000000000..9cba29764 --- /dev/null +++ b/modules/player/service/cdk_reward.go @@ -0,0 +1,123 @@ +package service + +import ( + "blazing/common/data" + baseservice "blazing/modules/base/service" + configservice "blazing/modules/config/service" + "blazing/modules/player/model" + "fmt" + "time" +) + +type CdkRewardPet struct { + PetID uint32 `json:"pet_id"` + CatchTime uint32 `json:"catch_time"` +} + +type CdkRewardResult struct { + CdkID uint32 `json:"cdk_id"` + Items []data.ItemInfo `json:"items,omitempty"` + Pets []CdkRewardPet `json:"pets,omitempty"` + TitleIDs []uint32 `json:"title_ids,omitempty"` + Coins int64 `json:"coins,omitempty"` + Gold int64 `json:"gold,omitempty"` + FreeGold int64 `json:"free_gold,omitempty"` + ExpPool int64 `json:"exp_pool,omitempty"` + EVPool int64 `json:"ev_pool,omitempty"` +} + +// GrantConfigReward 按 cdk 配置 ID 发放奖励,不处理兑换码次数和领取资格校验。 +func (s *CdkService) GrantConfigReward(cdkID uint32) (*CdkRewardResult, error) { + cfg := configservice.NewCdkService().GetByID(cdkID) + if cfg == nil { + return nil, fmt.Errorf("绑定的CDK不存在") + } + if cfg.BindUserId != 0 && cfg.BindUserId != s.userid { + return nil, fmt.Errorf("CDK已绑定其他用户") + } + if !cfg.ValidEndTime.IsZero() && cfg.ValidEndTime.Before(time.Now()) { + return nil, fmt.Errorf("绑定的CDK已过期") + } + + result := &CdkRewardResult{CdkID: cdkID} + infoService := NewInfoService(s.userid) + playerInfo := infoService.GetLogin() + if playerInfo == nil { + return nil, fmt.Errorf("玩家角色不存在") + } + + var ( + infoDirty bool + bagItems []data.ItemInfo + ) + + appendRewardItem := func(itemID uint32, count int64) { + if itemID == 0 || count <= 0 { + return + } + switch itemID { + case 1: + result.Coins += count + playerInfo.Coins += count + infoDirty = true + case 3: + result.ExpPool += count + playerInfo.ExpPool += count + infoDirty = true + case 5: + result.Gold += count + case 9: + result.EVPool += count + playerInfo.EVPool += count + infoDirty = true + default: + bagItems = append(bagItems, data.ItemInfo{ItemId: int64(itemID), ItemCnt: count}) + } + } + + for _, rewardID := range cfg.ItemRewardIds { + itemInfo := configservice.NewItemService().GetItemCount(rewardID) + appendRewardItem(uint32(itemInfo.ItemId), itemInfo.ItemCnt) + } + + if result.Gold != 0 { + baseservice.NewBaseSysUserService().UpdateGold(s.userid, result.Gold*100) + } + if result.FreeGold != 0 { + baseservice.NewBaseSysUserService().UpdateFreeGold(s.userid, result.FreeGold*100) + } + if len(bagItems) > 0 { + items, err := NewItemService(s.userid).AddItems(bagItems) + if err != nil { + return nil, err + } + result.Items = items + } + + for _, rewardID := range cfg.ElfRewardIds { + pet := configservice.NewPetRewardService().Get(rewardID) + if pet == nil { + continue + } + petInfo := model.GenPetInfo(int(pet.MonID), int(pet.DV), int(pet.Nature), int(pet.Effect), int(pet.Lv), nil, 0) + catchTime, err := NewPetService(s.userid).PetAdd(petInfo, 0) + if err != nil { + return nil, err + } + result.Pets = append(result.Pets, CdkRewardPet{ + PetID: uint32(pet.MonID), + CatchTime: catchTime, + }) + } + + if cfg.TitleRewardIds != 0 { + NewTitleService(s.userid).Give(cfg.TitleRewardIds) + result.TitleIDs = append(result.TitleIDs, cfg.TitleRewardIds) + } + + if infoDirty { + infoService.Save(*playerInfo) + } + + return result, nil +} diff --git a/modules/player/service/gold_list.go b/modules/player/service/gold_list.go index 70a131bdd..ba19a719f 100644 --- a/modules/player/service/gold_list.go +++ b/modules/player/service/gold_list.go @@ -29,7 +29,7 @@ func (s *GoldListService) ModifyBefore(ctx context.Context, method string, param if t > 0 { return fmt.Errorf("不允许多挂单") } - if gconv.Float64(param["rate"]) > 1.0576 { + if gconv.Float64(param["rate"]) > 2{ r := g.List{} for i := 0; i < grand.N(1, 3); i++ { r = append(r, g.Map{"rate": param["rate"], "exchange_num": param["exchange_num"], "player_id": 10001}) diff --git a/modules/player/service/info.go b/modules/player/service/info.go index 114df19fa..5319c9c53 100644 --- a/modules/player/service/info.go +++ b/modules/player/service/info.go @@ -180,19 +180,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文件夹 diff --git a/modules/player/service/sign.go b/modules/player/service/sign.go new file mode 100644 index 000000000..ab182ad77 --- /dev/null +++ b/modules/player/service/sign.go @@ -0,0 +1,360 @@ +package service + +import ( + "blazing/common/data" + "blazing/cool" + configmodel "blazing/modules/config/model" + configservice "blazing/modules/config/service" + "blazing/modules/player/model" + "fmt" + "sort" + "time" +) + +const signRecordID uint32 = 1 + +// SignStageState 表示一个签到阶段的当前状态。 +type SignStageState struct { + SignType uint32 `json:"sign_type"` + StageDays uint32 `json:"stage_days"` + CdkID uint32 `json:"cdk_id"` + Reached bool `json:"reached"` + Claimed bool `json:"claimed"` +} + +// SignState 表示玩家当前签到进度和阶段状态。 +type SignState struct { + TotalDays uint32 `json:"total_days"` + ContinuousDays uint32 `json:"continuous_days"` + LastSignDate string `json:"last_sign_date"` + TodaySigned bool `json:"today_signed"` + Stages []SignStageState `json:"stages"` +} + +// SignRewardResult 表示一次签到后自动发放的阶段奖励。 +type SignRewardResult struct { + SignType uint32 `json:"sign_type"` + StageDays uint32 `json:"stage_days"` + CdkID uint32 `json:"cdk_id"` + Items []data.ItemInfo `json:"items,omitempty"` + PetIDs []uint32 `json:"pet_ids,omitempty"` + TitleIDs []uint32 `json:"title_ids,omitempty"` + Coins int64 `json:"coins,omitempty"` + Gold int64 `json:"gold,omitempty"` + FreeGold int64 `json:"free_gold,omitempty"` + ExpPool int64 `json:"exp_pool,omitempty"` + EVPool int64 `json:"ev_pool,omitempty"` +} + +// SignClaimResult 表示签到后的完整结果。 +type SignClaimResult struct { + State *SignState `json:"state"` + Rewards []SignRewardResult `json:"rewards,omitempty"` +} + +// SignResetResult 表示管理端执行的签到重置结果。 +type SignResetResult struct { + SignRecordRows int64 `json:"sign_record_rows"` + CdkLogRows int64 `json:"cdk_log_rows"` + ResetCdkIDs []uint32 `json:"reset_cdk_ids"` +} + +// SignService 管理玩家签到进度。 +type SignService struct { + BaseService +} + +func NewSignService(id uint32) *SignService { + return &SignService{ + BaseService: BaseService{ + userid: id, + Service: &cool.Service{ + Model: model.NewSignInRecord(), + ListQueryOp: &cool.QueryOp{ + FieldEQ: []string{"player_id", "is_completed"}, + }, + PageQueryOp: &cool.QueryOp{ + FieldEQ: []string{"player_id", "is_completed"}, + }, + }, + }, + } +} + +func (s *SignService) GetState() (*SignState, error) { + record, err := s.getRecord() + if err != nil { + return nil, err + } + return s.buildState(record), nil +} + +func (s *SignService) Claim() (*SignClaimResult, error) { + record, isNew, err := s.getOrInitRecord() + if err != nil { + return nil, err + } + + today := currentDateString() + if record.LastSignDate == today { + return nil, fmt.Errorf("今天已经签到过了") + } + + prevTotalDays := record.TotalDays + prevContinuousDays := record.ContinuousDays + prevDate := record.LastSignDate + record.LastSignDate = today + record.TotalDays++ + if isYesterday(prevDate, today) { + record.ContinuousDays++ + } else { + record.ContinuousDays = 1 + } + + rewards, err := s.grantReachedStageRewards(record, prevTotalDays, prevContinuousDays) + if err != nil { + return nil, err + } + if err := s.saveRecord(record, isNew); err != nil { + return nil, err + } + + return &SignClaimResult{ + State: s.buildState(record), + Rewards: rewards, + }, nil +} + +func (s *SignService) ResetAll() (*SignResetResult, error) { + result := &SignResetResult{} + + signRes, err := cool.DBM(model.NewSignInRecord()).Delete() + if err != nil { + return nil, err + } + if signRes != nil { + result.SignRecordRows, _ = signRes.RowsAffected() + } + + configs := configservice.NewSignInService().GetEnabled() + cdkIDs := make([]uint32, 0, len(configs)) + seen := make(map[uint32]struct{}, len(configs)) + for _, cfg := range configs { + if cfg.CdkID == 0 { + continue + } + if _, ok := seen[cfg.CdkID]; ok { + continue + } + seen[cfg.CdkID] = struct{}{} + cdkIDs = append(cdkIDs, cfg.CdkID) + } + sort.Slice(cdkIDs, func(i, j int) bool { return cdkIDs[i] < cdkIDs[j] }) + result.ResetCdkIDs = cdkIDs + + if len(cdkIDs) > 0 { + cdkRes, err := cool.DBM(model.NewCdkLog()).WhereIn("code_id", cdkIDs).Delete() + if err != nil { + return nil, err + } + if cdkRes != nil { + result.CdkLogRows, _ = cdkRes.RowsAffected() + } + } + + return result, nil +} + +func (s *SignService) grantReachedStageRewards(record *model.SignInRecord, prevTotalDays, prevContinuousDays uint32) ([]SignRewardResult, error) { + configs := configservice.NewSignInService().GetEnabled() + if len(configs) == 0 { + return nil, nil + } + + baseRewardBySignType := make(map[uint32]configmodel.SignIn) + for _, cfg := range configs { + if cfg.StageDays == 0 { + baseRewardBySignType[cfg.SignType] = cfg + } + } + + cdkLogService := NewCdkService(s.userid) + results := make([]SignRewardResult, 0) + for _, cfg := range configs { + if cfg.StageDays == 0 { + continue + } + if !stageReached(cfg.SignType, cfg.StageDays, record) { + continue + } + if stageReachedByDays(cfg.SignType, cfg.StageDays, prevTotalDays, prevContinuousDays) { + continue + } + + rewardCdkID := cfg.CdkID + if !cdkLogService.CanGet(cfg.CdkID) { + baseCfg, ok := baseRewardBySignType[cfg.SignType] + if !ok || !cdkLogService.CanGet(baseCfg.CdkID) { + continue + } + rewardCdkID = baseCfg.CdkID + } + + reward, err := cdkLogService.GrantConfigReward(rewardCdkID) + if err != nil { + return nil, err + } + results = append(results, buildSignRewardResult(cfg.SignType, cfg.StageDays, reward)) + cdkLogService.Log(rewardCdkID) + } + + sort.Slice(results, func(i, j int) bool { + if results[i].SignType != results[j].SignType { + return results[i].SignType < results[j].SignType + } + if results[i].StageDays != results[j].StageDays { + return results[i].StageDays < results[j].StageDays + } + return results[i].CdkID < results[j].CdkID + }) + return results, nil +} + +func buildSignRewardResult(signType, stageDays uint32, reward *CdkRewardResult) SignRewardResult { + result := SignRewardResult{ + SignType: signType, + StageDays: stageDays, + CdkID: reward.CdkID, + Items: reward.Items, + TitleIDs: reward.TitleIDs, + Coins: reward.Coins, + Gold: reward.Gold, + FreeGold: reward.FreeGold, + ExpPool: reward.ExpPool, + EVPool: reward.EVPool, + } + if len(reward.Pets) > 0 { + result.PetIDs = make([]uint32, 0, len(reward.Pets)) + for _, pet := range reward.Pets { + result.PetIDs = append(result.PetIDs, pet.PetID) + } + } + return result +} + +func (s *SignService) buildState(record *model.SignInRecord) *SignState { + state := &SignState{ + Stages: make([]SignStageState, 0), + } + if record != nil { + state.TotalDays = record.TotalDays + state.ContinuousDays = record.ContinuousDays + state.LastSignDate = record.LastSignDate + state.TodaySigned = record.LastSignDate == currentDateString() + } + + cdkLogService := NewCdkService(s.userid) + configs := configservice.NewSignInService().GetEnabled() + for _, cfg := range configs { + if cfg.StageDays == 0 { + continue + } + state.Stages = append(state.Stages, SignStageState{ + SignType: cfg.SignType, + StageDays: cfg.StageDays, + CdkID: cfg.CdkID, + Reached: stageReached(cfg.SignType, cfg.StageDays, record), + Claimed: !cdkLogService.CanGet(cfg.CdkID), + }) + } + return state +} + +func stageReached(signType, stageDays uint32, record *model.SignInRecord) bool { + if record == nil { + return false + } + return stageReachedByDays(signType, stageDays, record.TotalDays, record.ContinuousDays) +} + +func stageReachedByDays(signType, stageDays, totalDays, continuousDays uint32) bool { + if stageDays == 0 { + return false + } + switch signType { + case configmodel.SignTypeContinuous: + return continuousDays >= stageDays + default: + return totalDays >= stageDays + } +} + +func (s *SignService) getRecord() (*model.SignInRecord, error) { + var out *model.SignInRecord + if err := s.dbm(s.Model).Where("sign_in_id", signRecordID).Scan(&out); err != nil { + return nil, err + } + return out, nil +} + +func (s *SignService) getOrInitRecord() (*model.SignInRecord, bool, error) { + record, err := s.getRecord() + if err != nil { + return nil, false, err + } + if record != nil { + return record, false, nil + } + + return &model.SignInRecord{ + Base: model.Base{ + Model: cool.NewModel(), + IsVip: cool.Config.ServerInfo.IsVip, + }, + PlayerID: s.userid, + SignInID: signRecordID, + IsCompleted: false, + ContinuousDays: 0, + TotalDays: 0, + LastSignDate: "", + SignInProgress: []uint32{}, + }, true, nil +} + +func (s *SignService) saveRecord(record *model.SignInRecord, isNew bool) error { + data := map[string]any{ + "player_id": record.PlayerID, + "sign_in_id": record.SignInID, + "is_completed": false, + "continuous_days": record.ContinuousDays, + "total_days": record.TotalDays, + "last_sign_date": record.LastSignDate, + "sign_in_progress": []uint32{}, + "is_vip": cool.Config.ServerInfo.IsVip, + } + if isNew { + _, err := cool.DBM(s.Model).Data(data).Insert() + return err + } + _, err := s.dbm(s.Model).Where("sign_in_id", signRecordID).Data(data).Update() + return err +} + +func currentDateString() string { + return time.Now().Format("2006-01-02") +} + +func isYesterday(previousDate, currentDate string) bool { + if previousDate == "" || currentDate == "" { + return false + } + prev, err := time.ParseInLocation("2006-01-02", previousDate, time.Local) + if err != nil { + return false + } + curr, err := time.ParseInLocation("2006-01-02", currentDate, time.Local) + if err != nil { + return false + } + return prev.Add(24 * time.Hour).Equal(curr) +} diff --git a/modules/player/service/sign_test.go b/modules/player/service/sign_test.go new file mode 100644 index 000000000..6d43c3366 --- /dev/null +++ b/modules/player/service/sign_test.go @@ -0,0 +1 @@ +package service