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
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- name: Verify generated code
run: |
go generate ./server/block/
go generate ./server/block/ ./server/item/component/
git diff --exit-code

- name: Build deployment artifact
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- name: Verify generated code
run: |
go generate ./server/block/
go generate ./server/block/ ./server/item/component/
git diff --exit-code

deploy:
Expand Down
311 changes: 311 additions & 0 deletions cmd/generate/componentgen/generate/components.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
package generate

import (
"bytes"
"fmt"
"go/format"
"os"
"path/filepath"
"strings"
)

// generateComponentImplementations writes a single file containing every component
// struct definition plus its ComponentName() and Encode() methods, sourced from
// the manifest. Helper transform functions used by the encoders are emitted once.
func (g *Generator) generateComponentImplementations() error {
var buf bytes.Buffer
buf.WriteString("// Code generated by componentgen. DO NOT EDIT.\n")
buf.WriteString("package component\n\n")

// Collect nested types (name -> fields) referenced by slice/nested codecs.
nested, encoderTypes := collectNestedTypes()

// Emit each top-level component type and its methods.
for _, spec := range manifest {
emitStruct(&buf, spec.Struct, spec.Comment, spec.Fields)
emitComponentMethods(&buf, spec)
emitConstants(&buf, spec)
}

// Emit nested types (sorted for determinism).
for _, name := range sortedNestedNames(nested) {
emitStruct(&buf, name, commentsFor(nested, name), nested[name])
}

// Emit encode<Nested>() functions only for types actually encoded via
// slice/nested codecs (avoid unused functions for NameOrTags-only types).
for _, name := range sortedNestedNames(encoderTypes) {
emitNestedEncoder(&buf, name, encoderTypes[name])
}

// Emit shared helper functions.
emitHelpers(&buf)

return writeFormattedFile(filepath.Join(g.outputDir, "components_gen.go"), buf.Bytes())
}

// writeFormattedFile writes Go source formatted with gofmt so generated files
// are always gofmt-clean.
func writeFormattedFile(path string, src []byte) error {
formatted, err := format.Source(src)
if err != nil {
return err
}
return os.WriteFile(path, formatted, 0o644)
}

// collectNestedTypes builds name -> fields for every nested struct referenced by
// slice/nested codecs in the manifest, including nested-of-nested. It returns the
// full set of nested types plus the subset that require an encode<T>() function
// (those used via slice/nested codecs, not NameOrTags).
func collectNestedTypes() (map[string][]Field, map[string][]Field) {
nested := map[string][]Field{}
encoderTypes := map[string][]Field{}
var walk func([]Field, bool)
walk = func(fields []Field, needsEncoder bool) {
for _, f := range fields {
var nestedName string
childNeedsEncoder := false
switch f.Codec {
case CodecSlice, CodecNested:
nestedName = f.SliceElement
childNeedsEncoder = true
case CodecNameOrTags:
nestedName = f.SliceElement
}
if nestedName != "" && !isScalarType(nestedName) && !isBuiltin(nestedName) {
if _, ok := nested[nestedName]; !ok && len(f.Fields) > 0 {
nested[nestedName] = f.Fields
walk(f.Fields, childNeedsEncoder)
}
if childNeedsEncoder {
encoderTypes[nestedName] = f.Fields
}
}
}
}
for _, spec := range manifest {
walk(spec.Fields, false)
}
return nested, encoderTypes
}

func sortedNestedNames(nested map[string][]Field) []string {
names := make([]string, 0, len(nested))
for n := range nested {
names = append(names, n)
}
sortStrings(names)
return names
}

func commentsFor(nested map[string][]Field, name string) string {
return name + " represents a nested structure used by a component."
}

// emitStruct writes a struct declaration.
func emitStruct(buf *bytes.Buffer, name, comment string, fields []Field) {
if comment != "" {
fmt.Fprintf(buf, "// %s\n", comment)
}
fmt.Fprintf(buf, "type %s struct {\n", name)
for _, f := range fields {
fmt.Fprintf(buf, "\t%s %s\n", f.GoName, f.Type)
}
buf.WriteString("}\n\n")
}

// emitComponentMethods writes ComponentName() and Encode() for a component spec.
func emitComponentMethods(buf *bytes.Buffer, spec Spec) {
receiver := strings.ToLower(spec.Struct[:1])

fmt.Fprintf(buf, "func (%s %s) ComponentName() string { return %q }\n\n", receiver, spec.Struct, spec.Name)

fmt.Fprintf(buf, "func (%s %s) Encode() (map[string]any, error) {\n", receiver, spec.Struct)
buf.WriteString("\tdata := map[string]any{}\n")
for _, f := range spec.Fields {
emitFieldEncode(buf, receiver, f, 1)
}
buf.WriteString("\treturn data, nil\n")
buf.WriteString("}\n\n")
}

// emitConstants writes the named constant type (if any) and the constant group.
func emitConstants(buf *bytes.Buffer, spec Spec) {
if len(spec.ConstantGroup) == 0 {
return
}
if spec.ConstantType != "" {
fmt.Fprintf(buf, "// %s is a named string type for the %q slot values.\n", spec.ConstantType, spec.Name)
fmt.Fprintf(buf, "type %s string\n\n", spec.ConstantType)
}
buf.WriteString("const (\n")
for _, c := range spec.ConstantGroup {
fmt.Fprintf(buf, "\t%s\n", c)
}
buf.WriteString(")\n\n")
}

// emitFieldEncode writes the assignment for a single field into data.
func emitFieldEncode(buf *bytes.Buffer, receiver string, f Field, indent int) {
key := f.NBTName
if key == "" {
return
}

if f.OmitEmpty {
fmt.Fprintf(buf, "%sif %s {\n", strings.Repeat("\t", indent), omitCondition(receiver, f))
indent++
}
fmt.Fprintf(buf, "%sdata[%q] = %s\n", strings.Repeat("\t", indent), key, encodeExpr(receiver, f))
if f.OmitEmpty {
fmt.Fprintf(buf, "%s}\n", strings.Repeat("\t", indent-1))
}
}

// omitCondition returns the Go expression that determines whether a field is omitted.
func omitCondition(receiver string, f Field) string {
if strings.HasPrefix(f.Type, "[]") {
return "len(" + receiver + "." + f.GoName + ") > 0"
}
return receiver + "." + f.GoName + " != " + zeroValue(f.Type)
}

// encodeExpr returns the Go expression producing the encoded value for a field.
func encodeExpr(receiver string, f Field) string {
ref := receiver + "." + f.GoName
switch f.Codec {
case CodecStringSlice:
return "stringSlice(" + ref + ")"
case CodecRange:
return "rangeData(" + ref + ")"
case CodecRangeInt:
return "rangeIntData(" + ref + ")"
case CodecList3:
return "list3Data(" + ref + ")"
case CodecBanned:
return "bannedItemsData(" + ref + ")"
case CodecScalarSlice:
return "scalarSlice(" + ref + ")"
case CodecSlice:
if f.SliceElement == "" || isScalarType(f.SliceElement) {
return "scalarSlice(" + ref + ")"
}
return "encodeSlice(" + ref + ", encode" + f.SliceElement + ")"
case CodecNested:
return "encode" + f.SliceElement + "(" + ref + ")"
case CodecNameOrTags:
return "encodeNameOrTags(" + ref + ")"
default:
return ref
}
}

// zeroValue returns the zero value literal for the given type.
func zeroValue(t string) string {
switch {
case strings.HasPrefix(t, "["):
return "(" + t + "{})"
case strings.HasPrefix(t, "[]"):
return "nil"
case t == "string":
return "\"\""
case t == "bool":
return "false"
default:
return "0"
}
}

// isScalarType reports whether t is a primitive Go type.
func isScalarType(t string) bool {
switch t {
case "string", "int", "int32", "int64", "float32", "float64", "bool", "byte":
return true
}
return false
}

// isBuiltin reports whether a type is a builtin composite (array/slice/map).
func isBuiltin(t string) bool {
return strings.HasPrefix(t, "[") || strings.HasPrefix(t, "[]") || strings.HasPrefix(t, "map[")
}

// emitNestedEncoder writes an encode<Nested>() function.
func emitNestedEncoder(buf *bytes.Buffer, name string, fields []Field) {
fmt.Fprintf(buf, "func encode%s(v %s) map[string]any {\n", name, name)
buf.WriteString("\tdata := map[string]any{}\n")
for _, f := range fields {
emitFieldEncode(buf, "v", f, 1)
}
buf.WriteString("\treturn data\n")
buf.WriteString("}\n\n")
}

// emitHelpers writes the transform helper functions referenced by the encoders.
func emitHelpers(buf *bytes.Buffer) {
buf.WriteString("func stringSlice(s []string) []any {\n")
buf.WriteString("\tr := make([]any, len(s))\n")
buf.WriteString("\tfor i, v := range s {\n")
buf.WriteString("\t\tr[i] = v\n")
buf.WriteString("\t}\n")
buf.WriteString("\treturn r\n")
buf.WriteString("}\n\n")

buf.WriteString("func scalarSlice[T any](s []T) []any {\n")
buf.WriteString("\tr := make([]any, len(s))\n")
buf.WriteString("\tfor i, v := range s {\n")
buf.WriteString("\t\tr[i] = v\n")
buf.WriteString("\t}\n")
buf.WriteString("\treturn r\n")
buf.WriteString("}\n\n")

buf.WriteString("func encodeSlice[V any](vs []V, enc func(V) map[string]any) []any {\n")
buf.WriteString("\tr := make([]any, len(vs))\n")
buf.WriteString("\tfor i, v := range vs {\n")
buf.WriteString("\t\tr[i] = enc(v)\n")
buf.WriteString("\t}\n")
buf.WriteString("\treturn r\n")
buf.WriteString("}\n\n")

buf.WriteString("func rangeData(r [2]float32) map[string]any {\n")
buf.WriteString("\treturn map[string]any{\"min\": r[0], \"max\": r[1]}\n")
buf.WriteString("}\n\n")

buf.WriteString("func rangeIntData(r [2]int32) map[string]any {\n")
buf.WriteString("\treturn map[string]any{\"min\": r[0], \"max\": r[1]}\n")
buf.WriteString("}\n\n")

buf.WriteString("func list3Data(c [3]int32) []any {\n")
buf.WriteString("\treturn []any{c[0], c[1], c[2]}\n")
buf.WriteString("}\n\n")

buf.WriteString("func bannedItemsData(items []string) []any {\n")
buf.WriteString("\tbanned := make([]any, len(items))\n")
buf.WriteString("\tfor i, b := range items {\n")
buf.WriteString("\t\tbanned[i] = map[string]any{\"name\": b}\n")
buf.WriteString("\t}\n")
buf.WriteString("\treturn banned\n")
buf.WriteString("}\n\n")

buf.WriteString("func encodeNameOrTags(items []RepairItemEntry) []any {\n")
buf.WriteString("\tr := make([]any, len(items))\n")
buf.WriteString("\tfor i, item := range items {\n")
buf.WriteString("\t\tif item.Name != \"\" {\n")
buf.WriteString("\t\t\tr[i] = map[string]any{\"name\": item.Name}\n")
buf.WriteString("\t\t} else {\n")
buf.WriteString("\t\t\tr[i] = map[string]any{\"tags\": stringSlice(item.Tags)}\n")
buf.WriteString("\t\t}\n")
buf.WriteString("\t}\n")
buf.WriteString("\treturn r\n")
buf.WriteString("}\n\n")
}

// sortStrings sorts a string slice in place.
func sortStrings(s []string) {
for i := 1; i < len(s); i++ {
for j := i; j > 0 && s[j-1] > s[j]; j-- {
s[j-1], s[j] = s[j], s[j-1]
}
}
}
Loading