From 6a76540271733330e4cf8eb50007b78bb01c1a2d Mon Sep 17 00:00:00 2001 From: Mario Castro Date: Thu, 2 Jan 2025 13:16:28 +0100 Subject: [PATCH] Scripting first steps --- counter.go | 72 ++++++++++++++++++----- counter_prototype.go | 14 +++-- counter_template.go | 135 +++++++++++++++++++++++++++---------------- go.mod | 13 +++-- schemas.go | 9 ++- 5 files changed, 168 insertions(+), 75 deletions(-) diff --git a/counter.go b/counter.go index 54b6b14..c7e2562 100644 --- a/counter.go +++ b/counter.go @@ -13,6 +13,7 @@ import ( "dario.cat/mergo" "github.com/fogleman/gg" "github.com/pkg/errors" + "github.com/robertkrimen/otto" "github.com/thehivecorporation/log" ) @@ -54,14 +55,15 @@ type Counters []Counter type Metadata struct { // PublicIcon in a FOW counter is the visible icon for the enemy. Imagine an icon for the back // of a block in a Columbia game - CardImage *Image `json:"card_image,omitempty"` - Cost int `json:"cost,omitempty"` - PublicIcon *Image `json:"public_icon,omitempty"` - Side string `json:"side,omitempty"` - SkipCardGeneration bool `json:"skip_card_generation,omitempty"` - Title string `json:"title,omitempty"` - TitlePosition *int `json:"title_position,omitempty"` - External map[string]interface{} `json:"external,omitempty"` + CardImage *Image `json:"card_image,omitempty"` + Cost int `json:"cost,omitempty"` + PublicIcon *Image `json:"public_icon,omitempty"` + Side string `json:"side,omitempty"` + SkipCardGeneration bool `json:"skip_card_generation,omitempty"` + Title string `json:"title,omitempty"` + TitlePosition *int `json:"title_position,omitempty"` + External map[string]any `json:"external,omitempty"` + Scripts []string `json:"scripts,omitempty"` } type ImageExtraData struct { @@ -88,6 +90,10 @@ func (c *Counter) GetTextInPosition(i int) string { // filenumber: CounterTemplate.PositionNumberForFilename. So it will always be fixed number // position: The position of the text in the counter (0-16) // suffix: A suffix on the file. Constant +// +// Result: +// +// [sidename_][[Metadata.TitlePosition][_position text][_Metadata.Side][_Metadata.Title]][_PrototypeName][_filenumber][_suffix].png func (c *Counter) GenerateCounterFilename(sideName string, position int, filenamesInUse *sync.Map) { if c.Filename != "" { return @@ -102,7 +108,8 @@ func (c *Counter) GenerateCounterFilename(sideName string, position int, filenam name = c.GetTextInPosition(*c.Metadata.TitlePosition) } if name != "" { - b.WriteString(name + " ") + b.WriteString("_") + b.WriteString(name) } // This way, the positional based name will always be the first part of the filename // while the manual title will come later. This is useful when using prototypes so that @@ -110,22 +117,24 @@ func (c *Counter) GenerateCounterFilename(sideName string, position int, filenam name = "" if c.Metadata.Side != "" { + b.WriteString("_") b.WriteString(c.Metadata.Side) - b.WriteString(" ") } if c.Metadata.Title != "" { + b.WriteString("_") b.WriteString(c.Metadata.Title) - b.WriteString(" ") } } if name != "" { - b.WriteString(name + " ") + b.WriteString("_") + b.WriteString(name) } if c.PrototypeName != "" { - b.WriteString(c.PrototypeName + " ") + b.WriteString("_") + b.WriteString(c.PrototypeName) } res := b.String() @@ -155,6 +164,9 @@ func (c *Counter) GenerateCounterFilename(sideName string, position int, filenam if res == "" { res = fmt.Sprintf("%04d", filenumber) } + res = strings.Trim(res, "_") + res = strings.TrimSpace(res) + res = strings.Trim(res, "_") res = strings.TrimSpace(res) filenamesInUse.Store(res, true) @@ -286,7 +298,7 @@ func (c *Counter) mergeFrontAndBack() (*Counter, error) { if c.PrettyName == "" { byt, _ := json.MarshalIndent(c, "", " ") - return nil, fmt.Errorf("PrettyName was empty for counter:\n%s\n", string(byt)) + return nil, fmt.Errorf("prettyName was empty for counter:\n%s", string(byt)) } c.Back.PrettyName = c.PrettyName + "_back" @@ -297,3 +309,35 @@ func (c *Counter) mergeFrontAndBack() (*Counter, error) { return c.Back, nil } + +func (c *Counter) runCounterScript(script string) (*Counter, error) { + vm := otto.New() + + byt, err := json.Marshal(c) + if err != nil { + return nil, fmt.Errorf("could not marshal counter to json: %w", err) + } + + if err = vm.Set("counter", string(byt)); err != nil { + return nil, err + } + + if _, err = vm.Run(script); err != nil { + return nil, fmt.Errorf("could not run script: %w", err) + } + + val, err := vm.Get("output") + if err != nil { + return nil, fmt.Errorf("could not get output from script: %w", err) + } + if byt, err = val.MarshalJSON(); err != nil { + return nil, err + } + + newCounter := &Counter{} + if err = json.Unmarshal(byt, newCounter); err != nil { + return nil, err + } + + return newCounter, nil +} diff --git a/counter_prototype.go b/counter_prototype.go index c54adb8..33abc91 100644 --- a/counter_prototype.go +++ b/counter_prototype.go @@ -14,10 +14,10 @@ import ( type CounterPrototype struct { Counter - ImagePrototypes []ImagePrototype `json:"image_prototypes,omitempty"` - TextPrototypes []TextPrototype `json:"text_prototypes,omitempty"` - Back *CounterPrototype `json:"back,omitempty"` - Metadata map[string]interface{} `json:"external,omitempty"` + ImagePrototypes []ImagePrototype `json:"image_prototypes,omitempty"` + TextPrototypes []TextPrototype `json:"text_prototypes,omitempty"` + Back *CounterPrototype `json:"back,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` } type ImagePrototype struct { @@ -48,6 +48,9 @@ func (p *CounterPrototype) ToCounters(filenamesInUse *sync.Map, sideName, protot return nil, err } newCounter.PrototypeName = prototypeName + newCounter.Metadata = &Metadata{} + newCounter.Metadata.Scripts = make([]string, len(p.Metadata.Scripts)) + copy(newCounter.Metadata.Scripts, p.Metadata.Scripts) if err = p.applyPrototypes(&newCounter, i); err != nil { return nil, err @@ -170,6 +173,9 @@ func mergeFrontAndBack(frontCounter *Counter, backProto *CounterPrototype, index backCounter.PrettyName = frontCounter.PrettyName + "_back" backCounter.Filename = strings.TrimSuffix(frontCounter.Filename, path.Ext(frontCounter.Filename)) + "_back.png" + backCounter.Metadata = &Metadata{} + backCounter.Metadata.Scripts = make([]string, len(frontCounter.Metadata.Scripts)) + copy(backCounter.Metadata.Scripts, frontCounter.Metadata.Scripts) images, err := cloneSlice(frontCounter.Images) if err != nil { diff --git a/counter_template.go b/counter_template.go index 085b2ba..d2200da 100644 --- a/counter_template.go +++ b/counter_template.go @@ -1,6 +1,7 @@ package counters import ( + "bytes" "encoding/json" "sort" "sync" @@ -30,7 +31,12 @@ type CounterTemplate struct { Counters []Counter `json:"counters,omitempty"` Prototypes map[string]CounterPrototype `json:"prototypes,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` + Metadata CounterTemplateMetadata `json:"metadata,omitempty"` +} + +type CounterTemplateMetadata struct { + External map[string]any `json:"external,omitempty"` + Scripts []string `json:"scripts,omitempty"` } type VassalCounterTemplateSettings struct { @@ -43,6 +49,10 @@ type VassalCounterTemplateSettings struct { // ParseCounterTemplate reads a JSON file and parses it into a CounterTemplate after applying it some default settings (if not // present in the file) func ParseCounterTemplate(byt []byte, filenamesInUse *sync.Map) (t *CounterTemplate, err error) { + if bytes.Contains(byt, []byte("\n")) { + byt = bytes.ReplaceAll(byt, []byte("\n"), []byte("")) + } + if err = ValidateSchemaBytes[CounterTemplate](byt); err != nil { return nil, errors.Wrap(err, "JSON file is not valid") } @@ -143,77 +153,100 @@ func (t *CounterTemplate) ParsePrototype() (*CounterTemplate, error) { return nil, errors.Wrap(err, "could not parse JSON file") } + // TODO: Scripting + if newTemplate.Metadata.Scripts != nil { + for _, script := range newTemplate.Metadata.Scripts { + err := t.runTemplateScript(script) + if err != nil { + return nil, errors.Wrap(err, "error trying to run script") + } + } + } + for i, counter := range newTemplate.Counters { + if counter.Metadata != nil { + scripts := make([]string, len(counter.Metadata.Scripts)) + copy(scripts, counter.Metadata.Scripts) + for _, script := range scripts { + newCounter, err := counter.runCounterScript(script) + if err != nil { + return nil, errors.Wrap(err, "error trying to run script") + } + newTemplate.Counters[i] = *newCounter + } + } + } + return newTemplate, nil } func (t *CounterTemplate) ExpandPrototypeCounterTemplate(filenamesInUse *sync.Map) (*CounterTemplate, error) { - if t.Counters != nil { - total := len(t.Counters) - for i := 0; i < total; i++ { - counter := t.Counters[i] - if counter.Filename == "" { - t.Counters[i].GenerateCounterFilename(t.Vassal.SideName, t.PositionNumberForFilename, filenamesInUse) - // t.Counters[i].Filename = counter.Filename - } - - if counter.Back != nil { - backCounter, err := t.Counters[i].mergeFrontAndBack() - if err != nil { - return nil, err - } + total := len(t.Counters) + for i := 0; i < total; i++ { + counter := t.Counters[i] + if counter.Filename == "" { + t.Counters[i].GenerateCounterFilename(t.Vassal.SideName, t.PositionNumberForFilename, filenamesInUse) + // t.Counters[i].Filename = counter.Filename + } - t.Counters = append(t.Counters, *backCounter) - // t.Counters[i].Back = nil + if counter.Back != nil { + backCounter, err := t.Counters[i].mergeFrontAndBack() + if err != nil { + return nil, err } - if t.Vassal.SideName != "" { - err := t.Counters[i].ToVassal(t.Vassal.SideName) - if err != nil { - log.Warn("could not create vassal piece from counter", err) - } - } + t.Counters = append(t.Counters, *backCounter) + // t.Counters[i].Back = nil } - // JSON counters to Counters, check Prototype in CounterTemplate - if t.Prototypes != nil { - if t.Counters == nil { - t.Counters = make([]Counter, 0) + if t.Vassal.SideName != "" { + err := t.Counters[i].ToVassal(t.Vassal.SideName) + if err != nil { + log.Warn("could not create vassal piece from counter", err) } + } + } - // sort prototypes by name, to ensure consistent output filenames this is a small - // inconvenience, because iterating over maps in Go returns keys in random order - names := make([]string, 0, len(t.Prototypes)) - for name := range t.Prototypes { - names = append(names, name) - } - sort.Strings(names) + if t.Prototypes != nil { + if t.Counters == nil { + t.Counters = make([]Counter, 0) + } - for _, prototypeName := range names { - prototype := t.Prototypes[prototypeName] + // sort prototypes by name, to ensure consistent output filenames this is a small + // inconvenience, because iterating over maps in Go returns keys in random order + names := make([]string, 0, len(t.Prototypes)) + for name := range t.Prototypes { + names = append(names, name) + } + sort.Strings(names) - cts, err := prototype.ToCounters(filenamesInUse, t.Vassal.SideName, prototypeName, t.PositionNumberForFilename) - if err != nil { - return nil, err - } + for _, prototypeName := range names { + prototype := t.Prototypes[prototypeName] - t.Counters = append(t.Counters, cts...) + cts, err := prototype.ToCounters(filenamesInUse, t.Vassal.SideName, prototypeName, t.PositionNumberForFilename) + if err != nil { + return nil, err } - t.Prototypes = nil + t.Counters = append(t.Counters, cts...) } - if t.Counters != nil { - total := len(t.Counters) - for i := 0; i < total; i++ { - counter := t.Counters[i] - if counter.Filename == "" { - counter.GenerateCounterFilename(t.Vassal.SideName, t.PositionNumberForFilename, filenamesInUse) - t.Counters[i].Filename = counter.Filename - } + t.Prototypes = nil + } + + if t.Counters != nil { + total := len(t.Counters) + for i := 0; i < total; i++ { + counter := t.Counters[i] + if counter.Filename == "" { + counter.GenerateCounterFilename(t.Vassal.SideName, t.PositionNumberForFilename, filenamesInUse) + t.Counters[i].Filename = counter.Filename } } - } return t, nil } + +func (t *CounterTemplate) runTemplateScript(script string) error { + return nil +} diff --git a/go.mod b/go.mod index e0adb3f..4b46b3f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/sayden/counters go 1.23 require ( + dario.cat/mergo v1.0.1 github.com/a-h/templ v0.2.778 github.com/alecthomas/kong v0.5.0 github.com/buger/jsonparser v1.1.1 @@ -19,9 +20,11 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 github.com/pkg/errors v0.9.1 github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 + github.com/robertkrimen/otto v0.5.1 github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.9.0 github.com/thehivecorporation/log v1.8.5 + github.com/tidwall/sjson v1.2.5 github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/image v0.0.0-20201208152932-35266b937fa6 @@ -29,7 +32,6 @@ require ( ) require ( - dario.cat/mergo v1.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bytedance/sonic v1.11.6 // indirect @@ -51,7 +53,6 @@ require ( github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juju/errors v0.0.0-20210818161939-5560c4c073ff // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect @@ -71,6 +72,9 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/tidwall/gjson v1.14.2 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect @@ -78,11 +82,12 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.28.0 // indirect - golang.org/x/sync v0.8.0 // indirect + golang.org/x/sync v0.10.0 // indirect golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.35.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/schemas.go b/schemas.go index 3625341..1f7f6d8 100644 --- a/schemas.go +++ b/schemas.go @@ -1,6 +1,7 @@ package counters import ( + "bytes" "io" "os" @@ -17,7 +18,11 @@ func ValidateSchemaReader[S CounterTemplate | CardsTemplate](r io.Reader) error return ValidateSchemaBytes[S](byt) } -func ValidateSchemaBytes[S CounterTemplate | CardsTemplate](docByt []byte) error { +func ValidateSchemaBytes[S CounterTemplate | CardsTemplate](docByt []byte) (err error) { + if bytes.Contains(docByt, []byte("\n")) { + docByt = bytes.ReplaceAll(docByt, []byte("\n"), []byte("")) + } + reflector := new(jsonschema.Reflector) counterTemplateSchemaMarshaller := reflector.Reflect(new(S)) schemaByt, err := counterTemplateSchemaMarshaller.MarshalJSON() @@ -46,7 +51,7 @@ func ValidateSchemaAtPath[S CounterTemplate | CardsTemplate](inputPath string) e func validateResult(result *gojsonschema.Result) error { if !result.Valid() { - err := errors.New("JSON file is not valid\n") + err := errors.New("JSON file is not valid") for _, desc := range result.Errors() { err = errors.Wrap(err, "\n"+desc.String()) }