package main import ( "bytes" "flag" "fmt" "go/ast" "io" "log" "os" "sort" "strconv" "strings" "golang.org/x/tools/go/packages" ) func main() { out := flag.String("o", "", "output file for hash constants and methods") flag.Parse() if len(flag.Args()) != 1 { log.Fatalln("Must pass one package to produce block hashes for.") } cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles, } pkgs, err := packages.Load(cfg, flag.Args()[0]) if err != nil { log.Fatalln(err) } f, err := os.OpenFile(*out, os.O_CREATE|os.O_TRUNC|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { log.Fatalln(err) } for _, pkg := range pkgs { procPackage(pkg, f) } _ = f.Close() } func procPackage(pkg *packages.Package, w io.Writer) { b := &hashBuilder{ pkg: pkg, fields: make(map[string][]*ast.Field), aliases: make(map[string]string), handled: map[string]struct{}{}, funcs: map[string]*ast.FuncDecl{}, blockFields: map[string][]*ast.Field{}, } b.readStructFields(pkg) b.readFuncs(pkg) b.resolveBlocks() b.sortNames() b.writePackage(w) b.writeConstants(w) b.writeNextHash(w) b.writeMethods(w) } var ( packageFormat = "// Code generated by cmd/blockhash; DO NOT EDIT.\n\npackage %v\n\n" methodFormat = "\nfunc (%v%v) Hash() (uint64, uint64) {\n\treturn %v, %v\n}\n" constFormat = "\thash%v" ) type hashBuilder struct { pkg *packages.Package fields map[string][]*ast.Field funcs map[string]*ast.FuncDecl aliases map[string]string handled map[string]struct{} blockFields map[string][]*ast.Field names []string } // sortNames sorts the names of the blockFields map and stores them in a slice. func (b *hashBuilder) sortNames() { b.names = make([]string, 0, len(b.blockFields)) for name := range b.blockFields { b.names = append(b.names, name) } sort.Slice(b.names, func(i, j int) bool { return b.names[i] < b.names[j] }) } // writePackage writes the package at the top of the file. func (b *hashBuilder) writePackage(w io.Writer) { if _, err := fmt.Fprintf(w, packageFormat, b.pkg.Name); err != nil { log.Fatalln(err) } if _, err := fmt.Fprintf(w, "import \"github.com/df-mc/dragonfly/server/world\"\n\n"); err != nil { log.Fatalln(err) } } // writeConstants writes hash constants for every block to a file. func (b *hashBuilder) writeConstants(w io.Writer) { if _, err := fmt.Fprintln(w, "const ("); err != nil { log.Fatalln(err) } for i, name := range b.names { c := constFormat if i == 0 { c += " = iota" } if _, err := fmt.Fprintf(w, c+"\n", name); err != nil { log.Fatalln(err) } } if _, err := fmt.Fprintln(w, "\thashCustomBlockBase\n)"); err != nil { log.Fatalln(err) } } func (b *hashBuilder) writeNextHash(w io.Writer) { if _, err := fmt.Fprintln(w, "\n// customBlockBase represents the base hash for all custom blocks."); err != nil { log.Fatalln(err) } if _, err := fmt.Fprintln(w, "var customBlockBase = uint64(hashCustomBlockBase - 1)"); err != nil { log.Fatalln(err) } if _, err := fmt.Fprintln(w, "\n// NextHash returns the next free hash for custom blocks."); err != nil { log.Fatalln(err) } if _, err := fmt.Fprintln(w, "func NextHash() uint64 {\n\tcustomBlockBase++\n\treturn customBlockBase\n}"); err != nil { log.Fatalln(err) } } func (b *hashBuilder) writeMethods(w io.Writer) { for _, name := range b.names { fields := b.blockFields[name] var h string var bitSize int fun := b.funcs[name] var recvName string for _, n := range fun.Recv.List[0].Names { recvName = n.Name } pos := b.pkg.Fset.Position(fun.Body.Pos()) f, err := os.Open(pos.Filename) if err != nil { log.Fatalln(err) } body := make([]byte, fun.Body.End()-fun.Body.Pos()) if _, err := f.ReadAt(body, int64(pos.Offset)); err != nil { log.Fatalln(err) } _ = f.Close() for _, field := range fields { for _, fieldName := range field.Names { if !bytes.Contains(body, []byte(fieldName.Name)) { // Field was not used in the EncodeBlock method, so we can assume it's not a property and thus // should not be in the Hash method. continue } if !fieldName.IsExported() { continue } directives := make(map[string]string) if field.Doc != nil { for _, d := range field.Doc.List { const k = "//blockhash:" if index := strings.Index(d.Text, k); index != -1 { dir := strings.Split(d.Text[index+len(k):], " ") directives[dir[0]] = strings.Join(dir[1:], " ") } } } str, v := b.ftype(name, recvName+"."+fieldName.Name, field.Type, directives) if v == 0 { // Assume this field is not used in the hash. continue } if bitSize > 64 { log.Println("Hash size of block properties of", name, "exceeds 64 bits. Please look at this manually.") } else { if h == "" { h += str } else { h += " | " + str } if bitSize > 0 { h += "<<" + strconv.Itoa(bitSize) } } bitSize += v } } if bitSize == 0 { // No need to have a receiver name if we don't use any of the fields of the block. recvName = "" } if recvName != "" { recvName += " " } if h == "" { h = "0" } if _, err := fmt.Fprintf(w, methodFormat, recvName, name, "hash"+name, h); err != nil { log.Fatalln(err) } } } func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[string]string) (string, int) { var name string switch t := expr.(type) { case *ast.BasicLit: name = t.Value case *ast.Ident: name = t.Name case *ast.SelectorExpr: name = t.Sel.Name case *ast.StarExpr: return "", 0 // Ignore this field default: log.Fatalf("unknown field type %#v\n", expr) return "", 0 } switch name { case "bool": return "uint64(boolByte(" + s + "))", 1 case "int": return "uint64(" + s + ")", 8 case "Block": return "world.BlockHash(" + s + ")", 32 case "Attachment": if _, ok := directives["facing_only"]; ok { log.Println("Found directive: 'facing_only'") return "uint64(" + s + ".FaceUint8())", 3 } return "uint64(" + s + ".Uint8())", 5 case "GrindstoneAttachment": return "uint64(" + s + ".Uint8())", 2 case "WoodType", "LeavesType", "FlowerType", "DoubleFlowerType", "Colour": // Assuming these were all based on metadata, it should be safe to assume a bit size of 4 for this. return "uint64(" + s + ".Uint8())", 4 case "CoralType", "SkullType": return "uint64(" + s + ".Uint8())", 3 case "AnvilType", "SandstoneType", "PrismarineType", "StoneBricksType", "NetherBricksType", "FroglightType", "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType": return "uint64(" + s + ".Uint8())", 2 case "OreType", "FireType", "DoubleTallGrassType": return "uint64(" + s + ".Uint8())", 1 case "Direction", "Axis": return "uint64(" + s + ")", 2 case "Face": return "uint64(" + s + ")", 3 default: log.Println("Found unhandled field type", "'"+name+"'", "in block", structName+".", "Assuming this field is not included in block states. Please make sure this is correct or add the type to cmd/blockhash.") } return "", 0 } func (b *hashBuilder) resolveBlocks() { for bl, fields := range b.fields { if _, ok := b.funcs[bl]; ok { b.blockFields[bl] = fields } } } func (b *hashBuilder) readFuncs(pkg *packages.Package) { for _, f := range pkg.Syntax { ast.Inspect(f, b.readFuncDecls) } } func (b *hashBuilder) readFuncDecls(node ast.Node) bool { if fun, ok := node.(*ast.FuncDecl); ok { // If the function is called 'EncodeBlock' and the receiver is not nil, meaning the function is a method, this // is an implementation of the world.Block interface. if fun.Name.Name == "EncodeBlock" && fun.Recv != nil { b.funcs[fun.Recv.List[0].Type.(*ast.Ident).Name] = fun } } return true } func (b *hashBuilder) readStructFields(pkg *packages.Package) { for _, f := range pkg.Syntax { ast.Inspect(f, b.readStructs) } b.resolveEmbedded() b.resolveAliases() } func (b *hashBuilder) resolveAliases() { for name, alias := range b.aliases { b.fields[name] = b.findFields(alias) } } func (b *hashBuilder) findFields(structName string) []*ast.Field { for { if fields, ok := b.fields[structName]; ok { // Alias found in the fields map, so it referred to a struct directly. return fields } if nested, ok := b.aliases[structName]; ok { // The alias itself was an alias, so continue with the next. structName = nested continue } // Neither an alias nor a struct: Break as this isn't going to go anywhere. return nil } } func (b *hashBuilder) resolveEmbedded() { for name, fields := range b.fields { if _, ok := b.handled[name]; ok { // Don't handle if a previous run already handled this struct. continue } newFields := make([]*ast.Field, 0, len(fields)) for _, f := range fields { if len(f.Names) == 0 { // We're dealing with an embedded struct here. They're of the type ast.Ident. if ident, ok := f.Type.(*ast.Ident); ok { for _, af := range b.findFields(ident.Name) { if len(af.Names) == 0 { // The struct this referred is embedding a struct itself which hasn't yet been processed, // so we need to rerun and hope that struct is handled next. This isn't a very elegant way, // and could lead to a lot of runs, but in general it's fast enough and does the job. b.resolveEmbedded() return } } newFields = append(newFields, b.findFields(ident.Name)...) } } else { newFields = append(newFields, f) } } // Make sure a next run doesn't end up handling this struct again. b.handled[name] = struct{}{} b.fields[name] = newFields } } func (b *hashBuilder) readStructs(node ast.Node) bool { if s, ok := node.(*ast.TypeSpec); ok { switch t := s.Type.(type) { case *ast.StructType: b.fields[s.Name.Name] = t.Fields.List case *ast.Ident: // This is a type created something like 'type Andesite polishable': A type alias. We need to handle // these later, first parse all struct types. b.aliases[s.Name.Name] = t.Name } } return true }