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
196 changes: 196 additions & 0 deletions cmd/component/component.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// component.go: Go code generator for ECS component managers
// Usage: go run component.go
// Scans for structs with '//go:generate go tool component' and generates ids.go and managers.go

package main

import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
)

// Accepts: -shared, -std, -example in any combination/order
var (
generateRe = regexp.MustCompile(`//go:generate go tool component((?: -[a-z]+)*)`)
)

type Component struct {
StructName string
File string
Shared bool
Std bool
Example bool
}

func main() {
_, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
os.Exit(1)
}

files, err := filepath.Glob("*.go")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list Go files: %v\n", err)
os.Exit(1)
}

var components []Component
for _, file := range files {
if strings.HasSuffix(file, "_test.go") {
continue
}
comps := processFile(file)
components = append(components, comps...)
}

if len(components) > 0 {
generateIdsGo(components)
generateManagersGo(components)
}
}

// processFile parses a Go file and returns all components with a go:generate comment.
func processFile(filename string) []Component {
fset := token.NewFileSet()
fileAst, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse %s: %v\n", filename, err)
return nil
}

// Map line number to comment text
comments := map[int]string{}
for _, cg := range fileAst.Comments {
for _, c := range cg.List {
comments[fset.Position(c.Pos()).Line] = c.Text
}
}

var components []Component
for _, decl := range fileAst.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.TYPE {
continue
}
for _, spec := range gd.Specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
// Accept both struct and type alias
if _, ok := ts.Type.(*ast.StructType); !ok {
if _, ok := ts.Type.(*ast.Ident); !ok {
continue
}
}
// Check for go:generate comment immediately above
line := fset.Position(gd.Pos()).Line - 1
comment, ok := comments[line]
if !ok {
continue
}
matches := generateRe.FindStringSubmatch(comment)
if matches == nil {
continue
}
flags := matches[1]
// Split flags by space and check for each
flagSet := map[string]bool{}
for _, f := range strings.Fields(flags) {
flagSet[f] = true
}
components = append(components, Component{
StructName: ts.Name.Name,
File: filename,
Shared: flagSet["-shared"],
Std: flagSet["-std"],
Example: flagSet["-example"],
})
}
}
return components
}

// generateIdsGo creates ids.go for the detected components.
func generateIdsGo(components []Component) {
if len(components) == 0 {
return
}
pkgName := getPackageName(components[0].File)
var buf bytes.Buffer
buf.WriteString("// Code generated by component generator; DO NOT EDIT.\n")
buf.WriteString("\npackage " + pkgName + "\n\n")
buf.WriteString("import (\n")
if anyExample(components) {
buf.WriteString("\t\"gomp/stdcomponents\"\n")
} else {
buf.WriteString("\t\"gomp/pkg/ecs\"\n")
}
buf.WriteString(")\n\n")
buf.WriteString("const (\n")
if components[0].Example {
buf.WriteString("\t" + components[0].StructName + "ComponentId = iota + stdcomponents.StdLastComponentId\n")
for i := 1; i < len(components); i++ {
buf.WriteString("\t" + components[i].StructName + "ComponentId\n")
}
} else {
buf.WriteString("\tInvalidComponentId ecs.ComponentId = iota\n")
for _, c := range components {
buf.WriteString("\t" + c.StructName + "ComponentId\n")
}
buf.WriteString("\tStdLastComponentId\n")
}
buf.WriteString(")\n\n")
_ = os.WriteFile("ids.go", buf.Bytes(), 0644)

Check failure

Code scanning / gosec

Expect WriteFile permissions to be 0600 or less

Expect WriteFile permissions to be 0600 or less
}

// getPackageName returns the package name for a Go file.
func getPackageName(filename string) string {
fset := token.NewFileSet()
fileAst, err := parser.ParseFile(fset, filename, nil, parser.PackageClauseOnly)
if err != nil {
return "main"
}
return fileAst.Name.Name
}

// anyExample returns true if any component is marked as -example.
func anyExample(components []Component) bool {
for _, c := range components {
if c.Example {
return true
}
}
return false
}

// generateManagersGo creates managers.go for the detected components.
func generateManagersGo(components []Component) {
if len(components) == 0 {
return
}
pkgName := getPackageName(components[0].File)
var buf bytes.Buffer
buf.WriteString("// Code generated by componentgen; DO NOT EDIT.\n")
buf.WriteString("\npackage " + pkgName + "\n\n")
buf.WriteString("import (\n\t\"gomp/pkg/ecs\"\n)\n\n")

for _, c := range components {
if c.Shared {
buf.WriteString(fmt.Sprintf("type %[1]sComponentManager = ecs.SharedComponentManager[%[1]s]\n\n", c.StructName))
buf.WriteString(fmt.Sprintf("func New%[1]sComponentManager() %[1]sComponentManager {\n\treturn ecs.NewSharedComponentManager[%[1]s](%[1]sComponentId)\n}\n\n", c.StructName))
} else {
buf.WriteString(fmt.Sprintf("type %[1]sComponentManager = ecs.ComponentManager[%[1]s]\n\n", c.StructName))
buf.WriteString(fmt.Sprintf("func New%[1]sComponentManager() %[1]sComponentManager {\n\treturn ecs.NewComponentManager[%[1]s](%[1]sComponentId)\n}\n\n", c.StructName))
}
}
_ = os.WriteFile("managers.go", buf.Bytes(), 0644)

Check failure

Code scanning / gosec

Expect WriteFile permissions to be 0600 or less

Expect WriteFile permissions to be 0600 or less
}
2 changes: 1 addition & 1 deletion desktop-components.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func NewDesktopComponents() DesktopComponents {
Tint: stdcomponents.NewTintComponentManager(),
AnimationPlayer: stdcomponents.NewAnimationPlayerComponentManager(),
AnimationState: stdcomponents.NewAnimationStateComponentManager(),
RlTexturePro: stdcomponents.NewRlTextureProComponentManager(),
RlTexturePro: stdcomponents.NewRLTextureProComponentManager(),
Network: stdcomponents.NewNetworkComponentManager(),
}
}
Expand Down
9 changes: 1 addition & 8 deletions examples/new-api/components/asteroid-scene-manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,8 @@ Thank you for your support!

package components

import "gomp/pkg/ecs"

//go:generate go tool component -example
type AsteroidSceneManager struct {
PlayerScore int32
PlayerHp int32
}

type AsteroidSceneManagerComponentManager = ecs.ComponentManager[AsteroidSceneManager]

func NewAsteroidSceneManagerComponentManager() AsteroidSceneManagerComponentManager {
return ecs.NewComponentManager[AsteroidSceneManager](AsteroidSceneManagerComponentId)
}
9 changes: 1 addition & 8 deletions examples/new-api/components/asteroid-tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@ Thank you for your support!

package components

import "gomp/pkg/ecs"

//go:generate go tool component -example
type AsteroidTag struct {
}

type AsteroidComponentManager = ecs.ComponentManager[AsteroidTag]

func NewAsteroidTagComponentManager() AsteroidComponentManager {
return ecs.NewComponentManager[AsteroidTag](AsteroidTagComponentId)
}
15 changes: 2 additions & 13 deletions examples/new-api/components/audio.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ package components

import (
rl "github.com/gen2brain/raylib-go/raylib"
"gomp/pkg/ecs"
)

//go:generate go tool component -example
type SoundEffect struct {
// audio clip from assets
Clip *rl.Sound
Expand All @@ -34,21 +34,10 @@ type SoundEffect struct {
Pan float32
}

type SoundEffectsComponentManager = ecs.ComponentManager[SoundEffect]

func NewSoundEffectsComponentManager() SoundEffectsComponentManager {
return ecs.NewComponentManager[SoundEffect](SoundEffectManagerComponentId)
}

//go:generate go tool component -example
type SpatialAudio struct {
// follows raylib rules. Base is 1.0
Volume float32
// follows raylib rules. Base is 0.5, 1.0 is left, 0.0 is right
Pan float32
}

type SpatialAudioComponentManager = ecs.ComponentManager[SpatialAudio]

func NewSpatialAudioComponentManager() SpatialAudioComponentManager {
return ecs.NewComponentManager[SpatialAudio](SpatialAudioManagerComponentId)
}
9 changes: 1 addition & 8 deletions examples/new-api/components/bullet-tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,6 @@ Thank you for your support!

package components

import "gomp/pkg/ecs"

//go:generate go tool component -example
type BulletTag struct {
}

type BulletTagComponentManager = ecs.ComponentManager[BulletTag]

func NewBulletTagComponentManager() BulletTagComponentManager {
return ecs.NewComponentManager[BulletTag](BulletTagComponentId)
}
9 changes: 1 addition & 8 deletions examples/new-api/components/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,5 @@ Thank you for your support!

package components

import "gomp/pkg/ecs"

//go:generate go tool component -example
type Controller struct{}

type ControllerComponentManager = ecs.ComponentManager[Controller]

func NewControllerComponentManager() ControllerComponentManager {
return ecs.NewComponentManager[Controller](ControllerComponentId)
}
9 changes: 1 addition & 8 deletions examples/new-api/components/hp.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,7 @@ Thank you for your support!

package components

import "gomp/pkg/ecs"

//go:generate go tool component -example
type Hp struct {
Hp, MaxHp int32
}

type HpComponentManager = ecs.ComponentManager[Hp]

func NewHealthComponentManager() HpComponentManager {
return ecs.NewComponentManager[Hp](HealthComponentId)
}
34 changes: 11 additions & 23 deletions examples/new-api/components/ids.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading