Files
bl/logic/controller/Controller.go
xinian d2cd601802
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
更新说明
2026-04-09 13:11:59 +08:00

242 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

// Package controller 提供游戏逻辑的控制器层,负责处理各种游戏功能的请求分发。
// 包含用户、宠物、战斗、物品、地图、任务等各个模块的控制器方法。
// 通过反射和标签系统自动注册 cmd 处理方法,实现请求到处理函数的映射。
package controller
import (
"blazing/common/rpc"
"blazing/cool"
"blazing/logic/service/common"
"bytes"
"context"
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"github.com/gogf/gf/v2/os/glog"
"github.com/lunixbochs/struc"
"github.com/panjf2000/gnet/v2"
)
// Maincontroller 是控制器层共享变量。
var Maincontroller = &Controller{} //注入service
// Controller 分发cmd逻辑实现
type Controller struct {
UID uint32
RPCClient *struct {
Kick func(uint32) error
RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(rpc.PVPMatchJoinPayload) error
MatchCancel func(uint32) error
}
}
// ParseCmd 将字节数组数据解析到指定类型的变量中
// 该函数使用struc库进行数据解包操作
// 参数 data: 需要解析的字节数据
// 返回值: 解析后的指定类型实例
func ParseCmd[T any](data []byte) T {
var result T
// 使用struc.Unpack将字节数据解包到result变量中
struc.Unpack(bytes.NewReader(data), &result)
return result
}
// 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.Method(i)
methodType := methodValue.Type()
if methodType.NumIn() == 0 {
continue
}
reqArgType := methodType.In(0)
if reqArgType.Kind() != reflect.Ptr || reqArgType.Elem().Kind() != reflect.Struct {
glog.Warning(context.Background(), "方法首参必须为结构体指针", method.Name, "跳过注册")
continue
}
reqType := reqArgType.Elem()
binding := getCmdBinding(reqType)
for _, cmd := range binding.cmds {
if cmd == 0 {
glog.Warning(context.Background(), "方法参数必须包含CMD参数", method.Name, "跳过注册")
continue
}
if methodType.NumIn() != 2 {
glog.Warning(context.Background(), "方法参数数量必须为2", method.Name, "跳过注册")
continue
}
if !isGame && cmd > 1000 {
continue
}
if isGame && cmd < 1000 {
continue
}
if cool.Config.ServerInfo.IsDebug != 0 {
fmt.Println("注册方法", cmd, method.Name)
}
reqTypeForNew := reqType
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 {
panic(fmt.Sprintf("命令处理方法已存在,跳过注册 %d %s", cmd, method.Name))
}
cool.CmdCache[cmd] = cmdInfo
}
}
}
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()
}
return typ
}
// getCmdBinding 从结构体类型中提取绑定的cmd指令和头字段位置。
func getCmdBinding(typ reflect.Type) cmdBinding {
typ = normalizeStructType(typ)
if cached, ok := cmdTypeCache.Load(typ); ok {
return cached.(cmdBinding)
}
if typ.Kind() != reflect.Struct {
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
cmdTypeCache.Store(typ, binding)
return binding
}
if binding, ok := findCmdBinding(typ, make(map[reflect.Type]struct{})); ok {
cmdTypeCache.Store(typ, binding)
return binding
}
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
cmdTypeCache.Store(typ, binding)
return binding
}
func findCmdBinding(typ reflect.Type, visiting map[reflect.Type]struct{}) (cmdBinding, bool) {
typ = normalizeStructType(typ)
if typ.Kind() != reflect.Struct {
return cmdBinding{}, false
}
if _, seen := visiting[typ]; seen {
return cmdBinding{}, false
}
visiting[typ] = struct{}{}
defer delete(visiting, typ)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
cmdSlice, isHeader, err := parseCmdTagWithStructField(field)
if isHeader && err == nil {
return cmdBinding{
cmds: cmdSlice,
headerFieldIndex: append([]int(nil), field.Index...),
}, true
}
nestedTyp := normalizeStructType(field.Type)
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 cmdBinding{}, false
}
// parseCmdTagWithStructField 校验字段是否为TomeeHeader值/指针并解析cmd标签
// 参数 field: 结构体字段元信息
// 返回值: 解析后的cmd切片是否为目标类型解析失败错误
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, bool, error) {
if field.Type != targetType && !(field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType) {
return nil, false, nil
}
cmdStr := field.Tag.Get("cmd")
if cmdStr == "" {
return nil, true, fmt.Errorf("field %s cmd tag is empty", field.Name)
}
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)
}
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)",
field.Name, idx, err, s)
}
result = append(result, uint32(num))
if !found {
break
}
remain = next
}
return result, true, nil
}