From 3eb5bcd0543f8a35b52a3426d06a6118721bf525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=92=9F=E8=92=BB?= Date: Wed, 18 Jun 2025 08:47:23 +0800 Subject: [PATCH] more util types --- cmd/packetizer/codec.go.tmpl | 52 +++++ cmd/packetizer/go.mod | 14 ++ cmd/packetizer/go.sum | 10 + cmd/packetizer/main.go | 361 +++++++++++++++++++++++++++++++++++ net/packet/util.go | 93 +++++++++ 5 files changed, 530 insertions(+) create mode 100644 cmd/packetizer/codec.go.tmpl create mode 100644 cmd/packetizer/go.mod create mode 100644 cmd/packetizer/go.sum create mode 100644 cmd/packetizer/main.go diff --git a/cmd/packetizer/codec.go.tmpl b/cmd/packetizer/codec.go.tmpl new file mode 100644 index 0000000..c4cb7b3 --- /dev/null +++ b/cmd/packetizer/codec.go.tmpl @@ -0,0 +1,52 @@ +{{- /*gotype: github.com/Tnze/go-mc/cmd/packetizer.PackageInfo*/ -}} +// Code generated by packetizer.go; DO NOT EDIT. + +package {{ .Name }} + +import ( + "io" +{{- if .Imports }} +{{ range .Imports }} + "{{ . }}" +{{- end }} +{{- end }} + + pk "github.com/Tnze/go-mc/net/packet" +) +{{ range .Structs }} +func (c *{{ .Name }}) ReadFrom(r io.Reader) (int64, error) { +{{- if eq (len .Fields) 0 }} + return 0, nil +{{- else }} + var n int64 + var err error + var temp int64 + {{- range .Fields }} + temp, err = {{ generateTarget . }}.ReadFrom(r) + n += temp + if err != nil { + return n, err + } + {{- end }} + return n, err +{{- end }} +} + +func (c *{{ .Name }}) WriteTo(w io.Writer) (int64, error) { +{{- if eq (len .Fields) 0 }} + return 0, nil +{{- else }} + var n int64 + var err error + var temp int64 + {{- range .Fields }} + temp, err = {{ generateTarget . }}.WriteTo(w) + n += temp + if err != nil { + return n, err + } + {{- end }} + return n, err +{{- end }} +} +{{ end }} \ No newline at end of file diff --git a/cmd/packetizer/go.mod b/cmd/packetizer/go.mod new file mode 100644 index 0000000..593b52a --- /dev/null +++ b/cmd/packetizer/go.mod @@ -0,0 +1,14 @@ +module github.com/Tnze/go-mc/cmd/packetizer + +go 1.24 + +require ( + github.com/Tnze/go-mc v1.20.2 + golang.org/x/tools v0.34.0 +) + +require ( + github.com/google/uuid v1.3.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sync v0.15.0 // indirect +) diff --git a/cmd/packetizer/go.sum b/cmd/packetizer/go.sum new file mode 100644 index 0000000..45ee9f1 --- /dev/null +++ b/cmd/packetizer/go.sum @@ -0,0 +1,10 @@ +github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q= +github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= diff --git a/cmd/packetizer/main.go b/cmd/packetizer/main.go new file mode 100644 index 0000000..036f3da --- /dev/null +++ b/cmd/packetizer/main.go @@ -0,0 +1,361 @@ +package main + +import ( + "bytes" + _ "embed" + "flag" + "fmt" + "go/ast" + "go/token" + "go/types" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/Tnze/go-mc/net/packet" + "golang.org/x/tools/go/packages" +) + +type FieldInfo struct { + Name string + Type string + NeedConvert bool + ValidField bool + IsSlice bool + IsUnconvertible bool + IsFunc bool +} + +type StructInfo struct { + Name string + Package string + Fields []FieldInfo + Imports []string +} + +type PackageInfo struct { + File *ast.File + Pkg *packages.Package + CommentMap ast.CommentMap + + Name string + Path string + + Structs []StructInfo + Imports []string +} + +func parseTag(tag string) string { + if tag == "" { + return "" + } + + tag = strings.Trim(tag, "`") + + parts := strings.Split(tag, " ") + for _, part := range parts { + if strings.HasPrefix(part, "mc:") { + mcValue := strings.Trim(part[3:], `"`) + return mcValue + } + } + + return "" +} + +func shouldProcessStruct(commentMap ast.CommentMap, genDecl *ast.GenDecl) bool { + if groups, ok := commentMap[genDecl]; ok { + for _, cg := range groups { + for _, c := range cg.List { + content := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(c.Text), "//")) + if content == "codec:gen" { + return true + } + } + } + } + return false +} + +func analyzeFile(pkgInfo *PackageInfo) { + var pkField *types.Interface + typ, ok := packetFieldMap["Field"] + if ok { + pkField = typ.Underlying().(*types.Interface) + } + for _, decl := range pkgInfo.File.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.TYPE { + continue + } + if !shouldProcessStruct(pkgInfo.CommentMap, gen) { + continue + } + + for _, spec := range gen.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + st, ok := ts.Type.(*ast.StructType) + if !ok { + continue + } + + structInfo := StructInfo{ + Name: ts.Name.Name, + Package: pkgInfo.Pkg.Name, + Fields: []FieldInfo{}, + } + + for _, field := range st.Fields.List { + if len(field.Names) == 0 { + continue + } + + for _, name := range field.Names { + fi := FieldInfo{ + Name: name.Name, + } + + mcTag := parseTag(fieldTagValue(field)) + if mcTag == "-" { + continue + } + obj := pkgInfo.Pkg.TypesInfo.Defs[name] + if obj != nil { + t := obj.Type() + + if s, ok := t.(*types.Slice); ok { + fi.IsSlice = true + t = s.Elem() + if mcTag != "" { + if fieldType, ok := packetFieldMap[mcTag]; ok { + if _, ok := fieldType.Underlying().(*types.Slice); ok { + fi.IsSlice = false + fi.NeedConvert = true + t = fieldType + } + } + } + } + + if b, ok := t.(*types.Basic); ok { + basicType := getBasicType(b.Kind()) + if basicType != nil { + fi.NeedConvert = true + t = basicType + } + } + + typeString := types.TypeString(t, func(p *types.Package) string { + return "" + }) + fi.Type = typeString + + if mcTag != "" { + if fieldType, ok := packetFieldMap[mcTag]; ok && types.ConvertibleTo(t, fieldType) { + fi.NeedConvert = true + fi.Type = mcTag + } + if fieldType, ok := packetFieldMap[mcTag]; ok { + if sig, ok := fieldType.Underlying().(*types.Signature); ok { + if sig.Results().Len() == 1 { + fi.IsFunc = true + fi.Type = mcTag + t = sig.Results().At(0).Type() + } + } + } + } + + { + if types.Implements(t, pkField) || types.Implements(types.NewPointer(t), pkField) { + fi.ValidField = true + } + } + } + + structInfo.Fields = append(structInfo.Fields, fi) + } + } + + pkgInfo.Structs = append(pkgInfo.Structs, structInfo) + } + } +} + +func getBasicType(kind types.BasicKind) types.Type { + switch kind { + case types.Bool: + return packetFieldMap["Boolean"] + case types.Int: + return packetFieldMap["Int"] + case types.Int8: + return packetFieldMap["Byte"] + case types.Int16: + return packetFieldMap["Short"] + case types.Int32: + return packetFieldMap["Int"] + case types.Int64: + return packetFieldMap["Long"] + case types.Uint8: + return packetFieldMap["UnsignedByte"] + case types.Uint16: + return packetFieldMap["UnsignedShort"] + case types.Float32: + return packetFieldMap["Float"] + case types.Float64: + return packetFieldMap["Double"] + case types.String: + return packetFieldMap["String"] + case types.UntypedBool: + return packetFieldMap["Boolean"] + case types.UntypedInt: + return packetFieldMap["Int"] + case types.UntypedFloat: + return packetFieldMap["Double"] + case types.UntypedString: + return packetFieldMap["String"] + default: + return nil + } +} + +func fieldTagValue(f *ast.Field) string { + if f.Tag != nil { + return f.Tag.Value + } + return "" +} + +func generateFieldTarget(field FieldInfo) string { + pattern := fmt.Sprintf("(&c.%s)", field.Name) + if field.IsSlice { + pattern = "pk.Array" + pattern + } else if field.IsFunc { + pattern = "pk." + field.Type + pattern + } else if field.NeedConvert { + pattern = "(*pk." + field.Type + ")" + pattern + } + return pattern +} + +// 按包分組 +func groupByPackage(packages []*PackageInfo) map[string]*PackageInfo { + grouped := make(map[string]*PackageInfo) + + for _, pkg := range packages { + key := filepath.Dir(pkg.Path) + if existing, ok := grouped[key]; ok { + existing.Structs = append(existing.Structs, pkg.Structs...) + for _, imp := range pkg.Imports { + found := false + for _, existingImp := range existing.Imports { + if existingImp == imp { + found = true + break + } + } + if !found { + existing.Imports = append(existing.Imports, imp) + } + } + } else { + grouped[key] = pkg + } + } + + return grouped +} + +var packetFieldMap = map[string]types.Type{} + +//go:embed codec.go.tmpl +var codecTemplate string + +var tmpl *template.Template + +func init() { + _ = packet.Field(nil) + + funcMap := template.FuncMap{ + "generateTarget": generateFieldTarget, + } + + tmpl = template.Must(template.New("codecs").Funcs(funcMap).Parse(codecTemplate)) +} + +func main() { + dir := flag.String("dir", ".", "input directory to search for codec:gen tags") + flag.Parse() + + cfg := &packages.Config{ + Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedDeps, + Fset: token.NewFileSet(), + Dir: *dir, + Env: os.Environ(), + } + + pkgs, err := packages.Load(cfg, "github.com/Tnze/go-mc/net/packet", "./...") + if err != nil { + fmt.Fprintf(os.Stderr, "Error: failed to load packet package: %v\n", err) + os.Exit(1) + } + for _, pkg := range pkgs { + if pkg.PkgPath == "github.com/Tnze/go-mc/net/packet" { + scope := pkg.Types.Scope() + for _, name := range scope.Names() { + packetFieldMap[name] = scope.Lookup(name).Type() + } + break + } + } + + var infos []*PackageInfo + for _, pkg := range pkgs { + if pkg.PkgPath == "github.com/Tnze/go-mc/net/packet" { + continue + } + for _, file := range pkg.Syntax { + pf := cfg.Fset.Position(file.Pos()).Filename + if strings.HasSuffix(pf, "codecs.go") { + continue + } + + info := &PackageInfo{File: file, Pkg: pkg, CommentMap: ast.NewCommentMap(cfg.Fset, file, file.Comments), Name: pkg.Name, Path: pf} + analyzeFile(info) + if info.Structs == nil || len(info.Structs) == 0 { + continue + } + infos = append(infos, info) + } + } + + if len(infos) == 0 { + fmt.Println("No structs found with // codec:gen. Nothing to generate.") + return + } + + grouped := groupByPackage(infos) + fmt.Printf("Processing %d package groups...\n", len(grouped)) + + for dirPath, pkgInfo := range grouped { + var buf bytes.Buffer + if err := tmpl.Execute(&buf, pkgInfo); err != nil { + fmt.Fprintf(os.Stderr, "Template execution error: %v\n", err) + continue + } + + out := filepath.Join(dirPath, "codecs.go") + if err := os.WriteFile(out, buf.Bytes(), 0644); err != nil { + fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", out, err) + continue + } + fmt.Printf("Generated %s\n", out) + } + + fmt.Println("Code generation complete.") +} diff --git a/net/packet/util.go b/net/packet/util.go index 6e08319..0336d92 100644 --- a/net/packet/util.go +++ b/net/packet/util.go @@ -248,6 +248,99 @@ func (o OptionEncoder[T]) WriteTo(w io.Writer) (n int64, err error) { return n1 + n2, err } +type IDSet struct { + TagName Identifier + IDs []int32 +} + +func (i *IDSet) WriteTo(w io.Writer) (n int64, err error) { + if i.TagName != "" { + n1, err := VarInt(0).WriteTo(w) + if err != nil { + return n1, err + } + n2, err := i.TagName.WriteTo(w) + return n1 + n2, err + } else { + n1, err := VarInt(len(i.IDs) + 1).WriteTo(w) + if err != nil { + return n1, err + } + n2 := int64(0) + for _, id := range i.IDs { + temp, err := VarInt(id).WriteTo(w) + n2 += temp + if err != nil { + return n1 + n2, err + } + } + return n1 + n2, err + } +} + +func (i *IDSet) ReadFrom(r io.Reader) (n int64, err error) { + var v VarInt + n1, err := v.ReadFrom(r) + if err != nil { + return n1, err + } + if v == 0 { + n2, err := i.TagName.ReadFrom(r) + return n1 + n2, err + } + + i.IDs = make([]int32, int(v-1)) + var d VarInt + for j := 0; j < int(v-1); j++ { + n2, err := d.ReadFrom(r) + if err != nil { + return n1 + n2, err + } + n1 += n2 + i.IDs[j] = int32(n1) + } + return n1, err +} + +type OptID[T FieldEncoder, P fieldPointer[T]] struct { + Has bool + ID int32 + Val T +} + +func (o OptID[T, P]) WriteTo(w io.Writer) (n int64, err error) { + if o.Has { + o.ID++ + } + n1, err := (*VarInt)(&o.ID).WriteTo(w) + if err != nil || o.ID > 0 { + return n1, err + } + n2, err := o.Val.WriteTo(w) + return n1 + n2, err +} + +func (o *OptID[T, P]) ReadFrom(r io.Reader) (n int64, err error) { + n1, err := (*VarInt)(&o.ID).ReadFrom(r) + if o.ID > 0 { + o.ID-- + return n1, err + } + if err != nil { + return n1, err + } + n2, err := P(&o.Val).ReadFrom(r) + return n1 + n2, err +} + +// Pointer returns the pointer of Val if Has is true, otherwise return nil. +func (o *OptID[T, P]) Pointer() (p *T) { + if o.Has { + p = &o.Val + } + return +} + type Tuple []any // FieldEncoder, FieldDecoder or both (Field) // WriteTo write Tuple to io.Writer, panic when any of filed don't implement FieldEncoder