Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 331 additions & 0 deletions cmd/blockaccessor/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
package main

import (
"bytes"
"flag"
"go/ast"
"go/format"
"log"
"os"
"path/filepath"
"sort"
"text/template"

"golang.org/x/tools/go/packages"
)

// accessorSpec describes a generated getter/setter method pair. A block qualifies for a spec when it
// implements EncodeBlock and has a field named Field of the type Pkg.FieldType.
type accessorSpec struct {
// Iface is the name of the interface generated for the accessor pair, such as 'HasFacing'.
Iface string
// IfaceDoc is the doc comment written above the generated interface.
IfaceDoc string
// Method is the name of the generated setter method, such as 'WithFacing'.
Method string
// MethodDoc is the doc comment written above each generated setter method.
MethodDoc string
// Getter is the name of the generated getter method, such as 'FacingDirection'. It must differ from
// Field: a method cannot share its name with a struct field.
Getter string
// GetterDoc is the doc comment written above each generated getter method.
GetterDoc string
// Param is the name of the setter's parameter.
Param string
// ParamType is the qualified type of the setter's parameter, such as 'cube.Direction'.
ParamType string
// Field is the name of the struct field set by the setter.
Field string
// Pkg and FieldType are the package identifier and type name the field must have to qualify.
Pkg, FieldType string
}

var specs = []accessorSpec{
{
Iface: "HasFacing",
IfaceDoc: `HasFacing represents a block with a horizontal facing direction. Blocks that face any of the six
// block faces (such as torches, levers and hoppers) or that use an attachment do not implement it.`,
Method: "WithFacing",
MethodDoc: `WithFacing returns a copy of the block with its facing set to facing. It does not update any
// other blocks that the block may be part of, such as the second half of a bed or door.`,
Getter: "FacingDirection",
GetterDoc: `FacingDirection returns the horizontal direction the block faces.`,
Param: "facing", ParamType: "cube.Direction",
Field: "Facing", Pkg: "cube", FieldType: "Direction",
},
{
Iface: "HasAxis",
IfaceDoc: `HasAxis represents a block oriented along one of the three axes, such as logs and pillars.`,
Method: "WithAxis",
MethodDoc: `WithAxis returns a copy of the block with its axis set to axis.`,
Getter: "PillarAxis",
GetterDoc: `PillarAxis returns the axis the block is oriented along.`,
Param: "axis", ParamType: "cube.Axis",
Field: "Axis", Pkg: "cube", FieldType: "Axis",
},
{
Iface: "HasColour",
IfaceDoc: `HasColour represents a block that comes in the sixteen dye colours, such as wool and concrete. On
// some blocks, such as beds and banners, the colour is stored as block entity data rather than in the
// encoded block state.`,
Method: "WithColour",
MethodDoc: `WithColour returns a copy of the block with its colour set to colour.`,
Getter: "DyeColour",
GetterDoc: `DyeColour returns the dye colour of the block.`,
Param: "colour", ParamType: "item.Colour",
Field: "Colour", Pkg: "item", FieldType: "Colour",
},
}

var fileTemplate = template.Must(template.New("accessor").Parse(`// Code generated by cmd/blockaccessor; DO NOT EDIT.

package {{.PkgName}}

import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
)
{{range .Specs}}
// {{.IfaceDoc}}
type {{.Iface}} interface {
world.Block
{{.Getter}}() {{.ParamType}}
{{.Method}}({{.Param}} {{.ParamType}}) world.Block
}
{{end}}{{range .Methods}}
// {{.Spec.GetterDoc}}
func (b {{.Struct}}) {{.Spec.Getter}}() {{.Spec.ParamType}} {
return b.{{.Spec.Field}}
}

// {{.Spec.MethodDoc}}
func (b {{.Struct}}) {{.Spec.Method}}({{.Spec.Param}} {{.Spec.ParamType}}) world.Block {
b.{{.Spec.Field}} = {{.Spec.Param}}
return b
}
{{end}}`))

func main() {
out := flag.String("o", "", "output file for accessor interfaces and methods")
flag.Parse()

if len(flag.Args()) != 1 {
log.Fatalln("Must pass one package to produce accessor methods for.")
}
cfg := &packages.Config{
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedFiles,
}
pkgs, err := packages.Load(cfg, flag.Args()[0])
if err != nil {
log.Fatalln(err)
}
if len(pkgs) != 1 {
log.Fatalln("Expected to load exactly one package.")
}
procPackage(pkgs[0], *out)
}

func procPackage(pkg *packages.Package, out string) {
b := &accessorBuilder{
pkg: pkg,
out: filepath.Base(out),
fields: make(map[string][]*ast.Field),
aliases: make(map[string]string),
handled: map[string]struct{}{},
blocks: map[string]struct{}{},
methods: map[string]struct{}{},
}
b.readStructFields()
b.readFuncs()
buf := &bytes.Buffer{}
if err := fileTemplate.Execute(buf, struct {
PkgName string
Specs []accessorSpec
Methods []methodData
}{PkgName: pkg.Name, Specs: specs, Methods: b.resolveMethods()}); err != nil {
log.Fatalln(err)
}
src, err := format.Source(buf.Bytes())
if err != nil {
log.Fatalln(err)
}
if err := os.WriteFile(out, src, 0644); err != nil {
log.Fatalln(err)
}
}

// methodData holds the data needed to generate the accessor methods of one spec for one block struct.
type methodData struct {
Struct string
Spec accessorSpec
}

type accessorBuilder struct {
pkg *packages.Package
// out is the base name of the generated file, excluded when reading the package so that previously
// generated methods are not treated as hand-written ones.
out string
fields map[string][]*ast.Field
aliases map[string]string
handled map[string]struct{}
// blocks holds the names of all structs that implement EncodeBlock with a value receiver.
blocks map[string]struct{}
// methods holds 'Struct.Method' keys for every hand-written method in the package, used to avoid
// generating an accessor that would collide with an existing method.
methods map[string]struct{}
}

// resolveMethods returns the sorted list of accessor methods to generate, matching each spec against the
// fields of every block struct.
func (b *accessorBuilder) resolveMethods() []methodData {
var methods []methodData
for _, spec := range specs {
var names []string
for name := range b.blocks {
if !b.hasField(name, spec) {
continue
}
b.checkCollision(name, spec.Method)
b.checkCollision(name, spec.Getter)
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
methods = append(methods, methodData{Struct: name, Spec: spec})
}
}
return methods
}

// checkCollision exits with an error if the struct passed has a hand-written method with the name
// passed: these methods are generated exclusively, so that their behaviour cannot diverge per block.
func (b *accessorBuilder) checkCollision(structName, method string) {
if _, ok := b.methods[structName+"."+method]; ok {
log.Fatalln(structName + "." + method + " is generated by cmd/blockaccessor and must not be written by hand.")
}
}

// hasField reports whether the struct passed has a field with the name and type required by spec.
func (b *accessorBuilder) hasField(structName string, spec accessorSpec) bool {
for _, field := range b.fields[structName] {
for _, name := range field.Names {
if name.Name != spec.Field {
continue
}
if s, ok := field.Type.(*ast.SelectorExpr); ok {
if x, ok := s.X.(*ast.Ident); ok && x.Name == spec.Pkg && s.Sel.Name == spec.FieldType {
return true
}
}
}
}
return false
}

// syntax returns the syntax trees of the package, excluding the previously generated output file.
func (b *accessorBuilder) syntax() []*ast.File {
files := make([]*ast.File, 0, len(b.pkg.Syntax))
for _, f := range b.pkg.Syntax {
if filepath.Base(b.pkg.Fset.Position(f.Pos()).Filename) == b.out {
continue
}
files = append(files, f)
}
return files
}

func (b *accessorBuilder) readFuncs() {
for _, f := range b.syntax() {
ast.Inspect(f, b.readFuncDecls)
}
}

func (b *accessorBuilder) readFuncDecls(node ast.Node) bool {
fun, ok := node.(*ast.FuncDecl)
if !ok || fun.Recv == nil {
return true
}
// Only value receivers qualify: the generated accessors copy the receiver.
ident, ok := fun.Recv.List[0].Type.(*ast.Ident)
if !ok {
return true
}
b.methods[ident.Name+"."+fun.Name.Name] = struct{}{}
if fun.Name.Name == "EncodeBlock" {
b.blocks[ident.Name] = struct{}{}
}
return true
}

func (b *accessorBuilder) readStructFields() {
for _, f := range b.syntax() {
ast.Inspect(f, b.readStructs)
}
b.resolveEmbedded()
b.resolveAliases()
}

func (b *accessorBuilder) resolveAliases() {
for name, alias := range b.aliases {
b.fields[name] = b.findFields(alias)
}
}

func (b *accessorBuilder) 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 *accessorBuilder) resolveEmbedded() {
for name, fields := range b.fields {
if _, ok := b.handled[name]; ok {
continue
}
newFields := make([]*ast.Field, 0, len(fields))
for _, f := range fields {
if len(f.Names) == 0 {
if ident, ok := f.Type.(*ast.Ident); ok {
for _, af := range b.findFields(ident.Name) {
if len(af.Names) == 0 {
// The struct embedded has an embedded struct of its own: Resolve that one
// first by restarting the process.
b.resolveEmbedded()
return
}
}
newFields = append(newFields, b.findFields(ident.Name)...)
}
} else {
newFields = append(newFields, f)
}
}
b.handled[name] = struct{}{}
b.fields[name] = newFields
}
}

func (b *accessorBuilder) readStructs(node ast.Node) bool {
s, ok := node.(*ast.TypeSpec)
if !ok {
return true
}
switch t := s.Type.(type) {
case *ast.StructType:
b.fields[s.Name.Name] = t.Fields.List
case *ast.Ident:
b.aliases[s.Name.Name] = t.Name
}
return true
}
4 changes: 4 additions & 0 deletions cmd/blockhash/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[
return "uint64(" + s + ".FaceUint8())", 3
}
return "uint64(" + s + ".Uint8())", 5
case "OptionalColour":
return "uint64(" + s + ".Uint8())", 5
case "GrindstoneAttachment":
return "uint64(" + s + ".Uint8())", 2
case "WoodType", "LeavesType", "FlowerType", "DoubleFlowerType", "Colour", "MushroomType", "SeagrassType":
Expand All @@ -256,6 +258,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[
return "uint64(" + s + ".Uint8())", 2
case "OreType", "FireType", "DoubleTallGrassType":
return "uint64(" + s + ".Uint8())", 1
case "BambooLeafSize":
return "uint64(" + s + ".Uint8())", 2
case "Direction", "Axis":
return "uint64(" + s + ")", 2
case "Face":
Expand Down
Loading
Loading