From 19f1744b8680e9739a951f162a2f603441147502 Mon Sep 17 00:00:00 2001 From: binary cat Date: Fri, 28 Aug 2020 10:51:50 -0400 Subject: [PATCH 01/40] added RunCommand --- debugConsole.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/debugConsole.go b/debugConsole.go index 83eeb316..f623ad6d 100644 --- a/debugConsole.go +++ b/debugConsole.go @@ -240,3 +240,16 @@ func fullScreen(sub []string) { err := SetFullScreen(on) dlog.ErrorCheck(err) } + +// RunCommand runs a command added with AddCommand. +// It's intended use is making it easier to +// alias commands/subcommands. +// It returns an error if the command doesn't exist. +func RunCommand(cmd string, args ...string) error { + fn, ok := commands[cmd] + if ok == false { + return fmt.Errorf("Unknown command %s",cmd) + } + fn(args) + return nil +} From 2d569b27b119a4754f1cec3b4b9510a9d5d53ce7 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 19 Sep 2020 14:08:09 -0400 Subject: [PATCH 02/40] new mod for cutting a shape --- render/mod/cut.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/render/mod/cut.go b/render/mod/cut.go index 2cfc695b..a745866c 100644 --- a/render/mod/cut.go +++ b/render/mod/cut.go @@ -6,6 +6,7 @@ import ( "github.com/oakmound/oak/v2/alg" "github.com/oakmound/oak/v2/alg/floatgeom" + "github.com/oakmound/oak/v2/shape" ) // CutRound rounds the edges of the Modifiable with Bezier curves. @@ -65,12 +66,32 @@ func CutRound(xOff, yOff float64) Mod { } } +// CutShape unsets pixels that are not in the provided shape. +func CutShape(sh shape.Shape) Mod { + return func(rgba image.Image) *image.RGBA { + bds := rgba.Bounds() + newRgba := image.NewRGBA(bds) + newRect := sh.Rect(bds.Dx(), bds.Dy()) + + // start off as a copy + for x := bds.Min.X; x < bds.Max.X; x++ { + for y := bds.Min.Y; y < bds.Max.Y; y++ { + if newRect[x-bds.Min.X][y-bds.Min.Y] { + newRgba.Set(x, y, rgba.At(x, y)) + } + } + } + + return newRgba + } +} + // todo: this should not be in this package func pointBetween(p1, p2 floatgeom.Point2, f float64) floatgeom.Point2 { return floatgeom.Point2{p1.X()*(1-f) + p2.X()*f, p1.Y()*(1-f) + p2.Y()*f} } -//CutFn can reduce or add blank space to an input image. +// CutFn can reduce or add blank space to an input image. // Each input function decides the starting location or offset of a cut. func CutFn(xMod, yMod, wMod, hMod func(int) int) Mod { return func(rgba image.Image) *image.RGBA { From a67be870c9a53115db6b4b344022bc392d99f2bf Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 19 Sep 2020 14:09:44 -0400 Subject: [PATCH 03/40] new mod to safely and mods together technically implementers could have a local solution previously --- render/mod/mod.go | 19 +++++++++++++++++++ render/mod/mod_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/render/mod/mod.go b/render/mod/mod.go index a65eab43..1ff484a6 100644 --- a/render/mod/mod.go +++ b/render/mod/mod.go @@ -25,6 +25,25 @@ func And(ms ...Mod) Mod { } } +// SafeAnd removes any nil mods before passing the resultant set to the And function. +// It will also return a functional no-op if the mods passed in are all nil. +func SafeAnd(ms ...Mod) Mod { + i := 0 + for i < len(ms) { + if ms[i] == nil { + ms = append(ms[:i], ms[i+1:]...) + continue + } + i++ + } + if len(ms) == 0 { + return func(rgba image.Image) *image.RGBA { + return rgba.(*image.RGBA) + } + } + return And(ms...) +} + // Scale returns a scaled rgba. func Scale(xRatio, yRatio float64) Mod { return func(rgba image.Image) *image.RGBA { diff --git a/render/mod/mod_test.go b/render/mod/mod_test.go index 7aadfbf2..d041c7d1 100644 --- a/render/mod/mod_test.go +++ b/render/mod/mod_test.go @@ -24,6 +24,33 @@ func TestComposedModifications(t *testing.T) { assert.Equal(t, base, chained) } +func TestSafeCompose(t *testing.T) { + modList := []Mod{ + nil, + Zoom(2.0, 2.0, 2.0), + CutFromLeft(2, 2), + nil, + } + base := setAll(newrgba(3, 3), color.RGBA{255, 0, 0, 255}) + base = modList[1](base) + base = modList[2](base) + chained := setAll(newrgba(3, 3), color.RGBA{255, 0, 0, 255}) + + assert.NotEqual(t, base, chained) + mCombined := SafeAnd(modList...) + chained = mCombined(chained) + assert.Equal(t, base, chained) + + base = setAll(newrgba(3, 3), color.RGBA{255, 0, 0, 255}) + modList = []Mod{ + nil, + nil, + } + mCombined = SafeAnd(modList...) + assert.Equal(t, base, mCombined(base)) + +} + func TestAllModifications(t *testing.T) { in := setAll(newrgba(3, 3), color.RGBA{255, 0, 0, 255}) type filterCase struct { From 43002b401c407c0426d1c9daf2f341d9da8f3939 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 19 Sep 2020 14:10:44 -0400 Subject: [PATCH 04/40] first pass at buttons respecting shape this just updates the color representation mouse collision to come --- entities/x/btn/button.go | 11 ++++++++++- entities/x/btn/option.go | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/entities/x/btn/button.go b/entities/x/btn/button.go index cbce32b6..1ee1966f 100644 --- a/entities/x/btn/button.go +++ b/entities/x/btn/button.go @@ -5,11 +5,11 @@ import ( "strconv" "github.com/oakmound/oak/v2/dlog" - "github.com/oakmound/oak/v2/event" "github.com/oakmound/oak/v2/mouse" "github.com/oakmound/oak/v2/render" "github.com/oakmound/oak/v2/render/mod" + "github.com/oakmound/oak/v2/shape" ) // A Generator defines the variables used to create buttons from optional arguments @@ -36,6 +36,7 @@ type Generator struct { ListChoice *int Group *Group DisallowRevert bool + Shape shape.Shape } func defGenerator() Generator { @@ -73,6 +74,7 @@ func (g Generator) Generate() Btn { func (g Generator) generate(parent *Generator) Btn { var box render.Modifiable + // handle differnt renderable options that could be passed to the generator switch { case g.Toggle != nil: //Handles checks and other toggle situations @@ -108,9 +110,16 @@ func (g Generator) generate(parent *Generator) Btn { box = g.R case g.ProgressFunc != nil: box = render.NewGradientBox(int(g.W), int(g.H), g.Color, g.Color2, g.ProgressFunc) + if g.Shape != nil { + g.Mod = mod.SafeAnd(g.Mod, mod.CutShape(g.Shape)) + } default: box = render.NewColorBox(int(g.W), int(g.H), g.Color) + if g.Shape != nil { + g.Mod = mod.SafeAnd(g.Mod, mod.CutShape(g.Shape)) + } } + if !g.DisallowRevert { box = render.NewReverting(box) } diff --git a/entities/x/btn/option.go b/entities/x/btn/option.go index e60e0118..9fd3f833 100644 --- a/entities/x/btn/option.go +++ b/entities/x/btn/option.go @@ -4,6 +4,7 @@ import ( "image/color" "github.com/oakmound/oak/v2/mouse" + "github.com/oakmound/oak/v2/shape" "github.com/oakmound/oak/v2/event" "github.com/oakmound/oak/v2/render" @@ -168,3 +169,12 @@ func DisallowRevert() Option { return g } } + +// Shape sets the underlying mouse collision to only be respected if in shape. +// If color is responsible for arendering then it will be formed to this shape as well. +func Shape(s shape.Shape) Option { + return func(g Generator) Generator { + g.Shape = s + return g + } +} From 299901d0d0bbf1898edb39047cb31feface6c1a7 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 19 Sep 2020 21:40:25 -0400 Subject: [PATCH 05/40] button shape filters mouse events current impl based on a concept of reserved eventnames --- entities/x/btn/button.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/entities/x/btn/button.go b/entities/x/btn/button.go index 1ee1966f..97ace5b1 100644 --- a/entities/x/btn/button.go +++ b/entities/x/btn/button.go @@ -3,6 +3,7 @@ package btn import ( "image/color" "strconv" + "strings" "github.com/oakmound/oak/v2/dlog" "github.com/oakmound/oak/v2/event" @@ -140,6 +141,44 @@ func (g Generator) generate(parent *Generator) Btn { btn = NewBox(g.Cid, g.X, g.Y, g.W, g.H, box, g.Layers...) } + // Update underlying mousecollision binding to only respect clicks in the shape. + // If a finer control is needed then it may make sense to use this as a starting off point + // instead of expanding this section. + if g.Shape != nil { + + // extract keys prior to loop as the map will be permuted by the following operations + keys := make([]string, 0, len(g.Bindings)) + for k := range g.Bindings { + // We only really care about mouse events. + // In some ways this is dangerous of an implementer has defined events that start with mouse... + // but in that case they might not use g.Shape anyways. + if !strings.HasPrefix(k, "Mouse") { + continue + } + keys = append(keys, k) + } + for _, k := range keys { + curBind := g.Bindings[k] + if curBind == nil { + continue + } + // This could cause issues with name collisions but its unlikely and documentation should help make it even more unlikely. + filteredK := "Filtered" + k + g.Bindings[filteredK] = g.Bindings[k] + g.Bindings[k] = []event.Bindable{ + func(id int, button interface{}) int { + btn := event.GetEntity(id).(Btn) + mEvent := button.(mouse.Event) + bSpace := btn.GetSpace().Bounds() + if g.Shape.In(int(mEvent.X()-bSpace.Min.X()), int(mEvent.Y()-bSpace.Min.Y()), int(bSpace.W()), int(bSpace.H())) { + btn.Trigger(filteredK, mEvent) + } + return 0 + }, + } + } + } + for k, v := range g.Bindings { for _, b := range v { btn.Bind(b, k) From 8b2f68f6f5942d48108a7f3d14090e88c35c3bab Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 19 Sep 2020 21:58:18 -0400 Subject: [PATCH 06/40] dlog level string #140 --- dlog/levels.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dlog/levels.go b/dlog/levels.go index 112f21b8..789f914e 100644 --- a/dlog/levels.go +++ b/dlog/levels.go @@ -19,3 +19,7 @@ var logLevels = map[Level]string{ INFO: "INFO", VERBOSE: "VERBOSE", } + +func (l Level) String() string { + return logLevels[l] +} From a4de455c93549ca2217ffc1ed8666fb3d4a770a9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 19 Sep 2020 22:28:05 -0400 Subject: [PATCH 07/40] button shape should attempt cut prior to other mods This is mainly due to the highlight mod functionality and usage in button --- entities/x/btn/button.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entities/x/btn/button.go b/entities/x/btn/button.go index 97ace5b1..b2db0966 100644 --- a/entities/x/btn/button.go +++ b/entities/x/btn/button.go @@ -112,12 +112,12 @@ func (g Generator) generate(parent *Generator) Btn { case g.ProgressFunc != nil: box = render.NewGradientBox(int(g.W), int(g.H), g.Color, g.Color2, g.ProgressFunc) if g.Shape != nil { - g.Mod = mod.SafeAnd(g.Mod, mod.CutShape(g.Shape)) + g.Mod = mod.SafeAnd(mod.CutShape(g.Shape), g.Mod) } default: box = render.NewColorBox(int(g.W), int(g.H), g.Color) if g.Shape != nil { - g.Mod = mod.SafeAnd(g.Mod, mod.CutShape(g.Shape)) + g.Mod = mod.SafeAnd(mod.CutShape(g.Shape), g.Mod) } } From 49a49a2f93d1408ee1847a2f049278ae0fdf374b Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 20 Sep 2020 07:34:38 -0400 Subject: [PATCH 08/40] x\btn shape filter will pass any non-mouse event This allows for simulated clicks without data. Useful for selectors that trigger with nil data. --- entities/x/btn/button.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/entities/x/btn/button.go b/entities/x/btn/button.go index b2db0966..b6ccad77 100644 --- a/entities/x/btn/button.go +++ b/entities/x/btn/button.go @@ -168,7 +168,12 @@ func (g Generator) generate(parent *Generator) Btn { g.Bindings[k] = []event.Bindable{ func(id int, button interface{}) int { btn := event.GetEntity(id).(Btn) - mEvent := button.(mouse.Event) + mEvent, ok := button.(mouse.Event) + // If the passed event is not a mouse event dont filter on location. + // Main current use case is for nil events passed via simulated clicks. + if !ok { + btn.Trigger(filteredK, button) + } bSpace := btn.GetSpace().Bounds() if g.Shape.In(int(mEvent.X()-bSpace.Min.X()), int(mEvent.Y()-bSpace.Min.Y()), int(bSpace.W()), int(bSpace.H())) { btn.Trigger(filteredK, mEvent) From 37d680d0179f988ef3cbd96ca740163eef7bfc86 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 26 Sep 2020 13:21:57 -0400 Subject: [PATCH 09/40] chore reduce panic potential in debug console. --- debugConsole.go | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/debugConsole.go b/debugConsole.go index f623ad6d..d85b6626 100644 --- a/debugConsole.go +++ b/debugConsole.go @@ -149,6 +149,10 @@ func mouseDetails(nothing int, mevent interface{}) int { } func viewportCommands(tokenString []string) { + if len(tokenString) > 0 { + fmt.Println("Input must start with the name of the viewport operation to perform.") + return + } switch tokenString[0] { case "unlock": if viewportLocked { @@ -170,6 +174,10 @@ func viewportCommands(tokenString []string) { } func fadeCommands(tokenString []string) { + if len(tokenString) > 0 { + fmt.Println("Input must start with the name of the renderable to fade") + return + } toFade, ok := render.GetDebugRenderable(tokenString[0]) fadeVal := parseTokenAsInt(tokenString, 1, 255) if ok { @@ -181,6 +189,10 @@ func fadeCommands(tokenString []string) { func skipCommands(skipScene chan bool) func(tokenString []string) { return func(tokenString []string) { + if len(tokenString) != 1 { + fmt.Println("Input must be a single string from the following (\"scene\"). ") + return + } switch tokenString[0] { case "scene": skipScene <- true @@ -191,19 +203,29 @@ func skipCommands(skipScene chan bool) func(tokenString []string) { } func printCommands(tokenString []string) { - if i, err := strconv.Atoi(tokenString[0]); err == nil { - if i > 0 && event.HasEntity(i) { - e := event.GetEntity(i) - fmt.Println(reflect.TypeOf(e), e) - } else { - fmt.Println("No entity ", i) - } - } else { + if len(tokenString) != 1 { + fmt.Println("Input must be a single number that corresponds to an entity.") + return + } + i, err := strconv.Atoi(tokenString[0]) + if err != nil { fmt.Println("Unable to parse", tokenString[0]) + return } + if i > 0 && event.HasEntity(i) { + e := event.GetEntity(i) + fmt.Println(reflect.TypeOf(e), e) + } else { + fmt.Println("No entity ", i) + } + } func mouseCommands(tokenString []string) { + if len(tokenString) != 1 { + fmt.Println("Input must be a single string from the following (\"details\") ") + return + } switch tokenString[0] { case "details": event.GlobalBind(mouseDetails, "MouseRelease") @@ -248,7 +270,7 @@ func fullScreen(sub []string) { func RunCommand(cmd string, args ...string) error { fn, ok := commands[cmd] if ok == false { - return fmt.Errorf("Unknown command %s",cmd) + return fmt.Errorf("Unknown command %s", cmd) } fn(args) return nil From 84c6505969aaa32e319d781fb83f36c72eb99b7a Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 26 Sep 2020 13:35:35 -0400 Subject: [PATCH 10/40] it was getting annoying adding this all the time to scenes. Possible collision with existing command for help But this is a debug option so its probably ok? --- debugConsole.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/debugConsole.go b/debugConsole.go index d85b6626..b5396a64 100644 --- a/debugConsole.go +++ b/debugConsole.go @@ -92,6 +92,7 @@ func debugConsole(resetCh, skipScene chan bool, input io.Reader) { dlog.ErrorCheck(AddCommand("mouse", mouseCommands)) dlog.ErrorCheck(AddCommand("move", moveWindow)) dlog.ErrorCheck(AddCommand("fullscreen", fullScreen)) + dlog.ErrorCheck(AddCommand("help", printDebugCommands)) dlog.ErrorCheck(AddCommand("quit", func([]string) { Quit() })) } @@ -234,6 +235,10 @@ func mouseCommands(tokenString []string) { } } +func printDebugCommands(tokenString []string) { + fmt.Printf("Commands: %s\n", strings.Join(GetDebugKeys(), ", ")) +} + func moveWindow(in []string) { if len(in) < 4 { dlog.Error("Insufficient integer arguments for moving window") From f5c2f0921ebbc929bff9018cc667b450a5981a93 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 26 Sep 2020 13:40:26 -0400 Subject: [PATCH 11/40] sort the output of the debug helper --- debugConsole.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/debugConsole.go b/debugConsole.go index b5396a64..a28b9bbe 100644 --- a/debugConsole.go +++ b/debugConsole.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "reflect" + "sort" "strconv" "strings" @@ -236,7 +237,9 @@ func mouseCommands(tokenString []string) { } func printDebugCommands(tokenString []string) { - fmt.Printf("Commands: %s\n", strings.Join(GetDebugKeys(), ", ")) + dbgKeys := GetDebugKeys() + sort.Strings(dbgKeys) + fmt.Printf("Commands: %s\n", strings.Join(dbgKeys, ", ")) } func moveWindow(in []string) { From 88db8310374cea314617f16c8d75caaa9f124d3d Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 10 Oct 2020 11:47:01 -0500 Subject: [PATCH 12/40] Remove logging error prints These prints are very spammy when they happen and as far as we can tell don't report any actionable errors. --- dlog/default.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/dlog/default.go b/dlog/default.go index 17a77fe7..04fc6594 100644 --- a/dlog/default.go +++ b/dlog/default.go @@ -70,16 +70,8 @@ func (l *logger) dLog(console, override bool, in ...interface{}) { } if l.writer != nil { - _, err := l.writer.WriteString(l.byt.String()) - if err != nil { - // We can't log errors while we are in the error - // logging function. - fmt.Println("Logging error", err) - } - err = l.writer.Flush() - if err != nil { - fmt.Println("Logging error", err) - } + l.writer.WriteString(l.byt.String()) + l.writer.Flush() } l.byt.Reset() From 45ff7abcb1e0ce59e599dca421d18082acfdde52 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 17 Oct 2020 14:30:33 -0500 Subject: [PATCH 13/40] Parallelize audio and image batch loading --- audio/globals.go | 9 +++-- audio/load.go | 79 +++++++++++++++++++++++++++++--------------- dlog/default.go | 35 ++++++++++++-------- loading.go | 34 ++++++++++--------- render/batchload.go | 56 ++++++++++++++++--------------- render/loader.go | 7 ++-- render/loadsprite.go | 74 +++++++++++++++++++++-------------------- 7 files changed, 173 insertions(+), 121 deletions(-) diff --git a/audio/globals.go b/audio/globals.go index 1826e075..e5e23e73 100644 --- a/audio/globals.go +++ b/audio/globals.go @@ -1,9 +1,14 @@ package audio -import "github.com/200sc/klangsynthese/font" +import ( + "sync" + + "github.com/200sc/klangsynthese/font" +) var ( - loaded = make(map[string]Data) + loadedLock sync.RWMutex + loaded = make(map[string]Data) // DefFont is the font used for default functions. It can be publicly // modified to apply a default font to generated audios through def // methods. If it is not modified, it is a font of zero filters. diff --git a/audio/load.go b/audio/load.go index e8173653..097b8754 100644 --- a/audio/load.go +++ b/audio/load.go @@ -7,6 +7,7 @@ import ( "github.com/200sc/klangsynthese/audio" "github.com/200sc/klangsynthese/mp3" "github.com/200sc/klangsynthese/wav" + "golang.org/x/sync/errgroup" "github.com/oakmound/oak/v2/dlog" "github.com/oakmound/oak/v2/fileutil" @@ -49,42 +50,61 @@ func Get(filename string) (Data, error) { // one stored in the loeaded map. func Load(directory, filename string) (Data, error) { dlog.Verb("Loading", directory, filename) - if !IsLoaded(filename) { - f, err := fileutil.Open(filepath.Join(directory, filename)) - if err != nil { - return nil, err - } - defer f.Close() - var buffer audio.Audio - end := strings.ToLower(filename[len(filename)-4:]) - switch end { - case ".wav": - buffer, err = wav.Load(f) - case ".mp3": - buffer, err = mp3.Load(f) - default: - return nil, oakerr.UnsupportedFormat{Format: end} - } - if err != nil { - return nil, err - } - loaded[filename] = buffer.(audio.FullAudio) + if data, ok := getLoaded(filename); ok { + return data, nil + } + f, err := fileutil.Open(filepath.Join(directory, filename)) + if err != nil { + return nil, err + } + defer f.Close() + var buffer audio.Audio + end := strings.ToLower(filename[len(filename)-4:]) + switch end { + case ".wav": + buffer, err = wav.Load(f) + case ".mp3": + buffer, err = mp3.Load(f) + default: + return nil, oakerr.UnsupportedFormat{Format: end} + } + if err != nil { + return nil, err } - return loaded[filename], nil + data := buffer.(audio.FullAudio) + setLoaded(filename, data) + return data, nil } // Unload removes an element from the loaded map. If the element does not // exist, it does nothing. func Unload(filename string) { + loadedLock.Lock() delete(loaded, filename) + loadedLock.Unlock() } // IsLoaded is shorthand for (if _, ok := loaded[filename]; ok) func IsLoaded(filename string) bool { + loadedLock.RLock() _, ok := loaded[filename] + loadedLock.RUnlock() return ok } +func getLoaded(filename string) (Data, bool) { + loadedLock.RLock() + data, ok := loaded[filename] + loadedLock.RUnlock() + return data, ok +} + +func setLoaded(filename string, data Data) { + loadedLock.Lock() + loaded[filename] = data + loadedLock.Unlock() +} + // BatchLoad attempts to load all files within a given directory // depending on their file ending (currently supporting .wav and .mp3) func BatchLoad(baseFolder string) error { @@ -96,6 +116,7 @@ func BatchLoad(baseFolder string) error { return err } + var eg errgroup.Group for _, file := range files { if !file.IsDir() { n := file.Name() @@ -103,16 +124,20 @@ func BatchLoad(baseFolder string) error { switch strings.ToLower(n[len(n)-4:]) { case ".wav", ".mp3": dlog.Verb("loading file ", n) - _, err := Load(baseFolder, n) - if err != nil { - dlog.Error(err) - return err - } + eg.Go(func() error { + _, err := Load(baseFolder, n) + if err != nil { + dlog.Error(err) + return err + } + return nil + }) default: dlog.Error("Unsupported file ending for batchLoad: ", n) } } } + err = eg.Wait() dlog.Verb("Loading complete") - return nil + return err } diff --git a/dlog/default.go b/dlog/default.go index 04fc6594..fd6fa58a 100644 --- a/dlog/default.go +++ b/dlog/default.go @@ -8,11 +8,12 @@ import ( "runtime" "strconv" "strings" + "sync" "time" ) type logger struct { - byt *bytes.Buffer + bytPool sync.Pool debugLevel Level debugFilter string writer *bufio.Writer @@ -22,7 +23,11 @@ type logger struct { // no file, and level set to ERROR func NewLogger() Logger { return &logger{ - byt: bytes.NewBuffer(make([]byte, 0)), + bytPool: sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, + }, debugLevel: ERROR, } } @@ -51,30 +56,32 @@ func (l *logger) dLog(console, override bool, in ...interface{}) { return } + buffer := l.bytPool.Get().(*bytes.Buffer) // Note on errors: these functions all return // errors, but they are always nil. - l.byt.WriteRune('[') - l.byt.WriteString(f) - l.byt.WriteRune(':') - l.byt.WriteString(strconv.Itoa(line)) - l.byt.WriteString("] ") - l.byt.WriteString(logLevels[l.GetLogLevel()]) - l.byt.WriteRune(':') + buffer.WriteRune('[') + buffer.WriteString(f) + buffer.WriteRune(':') + buffer.WriteString(strconv.Itoa(line)) + buffer.WriteString("] ") + buffer.WriteString(logLevels[l.GetLogLevel()]) + buffer.WriteRune(':') for _, elem := range in { - l.byt.WriteString(fmt.Sprintf("%v ", elem)) + buffer.WriteString(fmt.Sprintf("%v ", elem)) } - l.byt.WriteRune('\n') + buffer.WriteRune('\n') if console { - fmt.Print(l.byt.String()) + fmt.Print(buffer.String()) } if l.writer != nil { - l.writer.WriteString(l.byt.String()) + l.writer.WriteString(buffer.String()) l.writer.Flush() } - l.byt.Reset() + buffer.Reset() + l.bytPool.Put(buffer) } } diff --git a/loading.go b/loading.go index 49c5f8f5..098f6e0e 100644 --- a/loading.go +++ b/loading.go @@ -5,6 +5,7 @@ import ( "github.com/oakmound/oak/v2/dlog" "github.com/oakmound/oak/v2/fileutil" "github.com/oakmound/oak/v2/render" + "golang.org/x/sync/errgroup" ) var ( @@ -16,21 +17,24 @@ var ( func loadAssets(imageDir, audioDir string) { if conf.BatchLoad { dlog.Info("Loading Images") - err := render.BatchLoad(imageDir) - if err != nil { - dlog.Error(err) - endLoad() - return - } - dlog.Info("Done Loading Images") - dlog.Info("Loading Audio") - err = audio.BatchLoad(audioDir) - if err != nil { - dlog.Error(err) - endLoad() - return - } - dlog.Info("Done Loading Audio") + var eg errgroup.Group + eg.Go(func() error { + err := render.BatchLoad(imageDir) + if err != nil { + return err + } + dlog.Info("Done Loading Images") + return nil + }) + eg.Go(func() error { + err := audio.BatchLoad(audioDir) + if err != nil { + return err + } + dlog.Info("Done Loading Audio") + return nil + }) + dlog.ErrorCheck(eg.Wait()) } endLoad() } diff --git a/render/batchload.go b/render/batchload.go index 9154144c..bc18f464 100644 --- a/render/batchload.go +++ b/render/batchload.go @@ -53,34 +53,38 @@ func BatchLoad(baseFolder string) error { if _, ok := fileDecoders[strings.ToLower(name[len(name)-4:])]; ok { dlog.Verb("loading file ", name) lower := strings.ToLower(name) + relativePath := filepath.Join(folder.Name(), name) if lower != name { - warnFiles = append(warnFiles, filepath.Join(folder.Name(), name)) - } - buff, err := loadSprite(baseFolder, filepath.Join(folder.Name(), name)) - if err != nil { - dlog.Error(err) - continue - } - w := buff.Bounds().Max.X - h := buff.Bounds().Max.Y - - dlog.Verb("buffer: ", w, h, " frame: ", frameW, frameH) - - if !possibleSheet { - continue - } else if w < frameW || h < frameH { - dlog.Error("File ", name, " in folder", folder.Name(), - " is too small for folder dimensions", frameW, frameH) - return errors.New("File in folder is too small for folder dimensions: " + - strconv.Itoa(w) + ", " + strconv.Itoa(h)) - - // Load this as a sheet if it is greater - // than the folder size's frame size - } else if w != frameW || h != frameH { - dlog.Verb("Loading as sprite sheet") - _, err = LoadSheet(baseFolder, filepath.Join(folder.Name(), name), frameW, frameH, defaultPad) - dlog.ErrorCheck(err) + warnFiles = append(warnFiles, relativePath) } + go func(baseFolder, relativePath string, possibleSheet bool, frameW, frameH int) { + buff, err := loadSprite(baseFolder, relativePath) + if err != nil { + dlog.Error(err) + return + } + + if !possibleSheet { + return + } + + w := buff.Bounds().Max.X + h := buff.Bounds().Max.Y + + dlog.Verb("buffer: ", w, h, " frame: ", frameW, frameH) + + if w < frameW || h < frameH { + dlog.Error("File ", name, " in folder", folder.Name(), + " is too small for folder dimensions", frameW, frameH) + + // Load this as a sheet if it is greater + // than the folder size's frame size + } else if w != frameW || h != frameH { + dlog.Verb("Loading as sprite sheet") + _, err = LoadSheet(baseFolder, relativePath, frameW, frameH, defaultPad) + dlog.ErrorCheck(err) + } + }(baseFolder, relativePath, possibleSheet, frameW, frameH) } else { dlog.Error("Unsupported file ending for batchLoad: ", name) } diff --git a/render/loader.go b/render/loader.go index f22f8d06..def7c9aa 100644 --- a/render/loader.go +++ b/render/loader.go @@ -15,12 +15,15 @@ var ( wd, "assets", "images") + + imageLock = sync.RWMutex{} loadedImages = make(map[string]*image.RGBA) + + sheetLock = sync.RWMutex{} loadedSheets = make(map[string]*Sheet) + // move to some batch load settings defaultPad = 0 - imageLock = sync.RWMutex{} - sheetLock = sync.RWMutex{} ) func subImage(rgba *image.RGBA, x, y, w, h int) *image.RGBA { diff --git a/render/loadsprite.go b/render/loadsprite.go index 6472e3d5..f3c9e850 100644 --- a/render/loadsprite.go +++ b/render/loadsprite.go @@ -13,48 +13,52 @@ import ( func loadSprite(directory, fileName string) (*image.RGBA, error) { - imageLock.Lock() - defer imageLock.Unlock() + imageLock.RLock() + if img, ok := loadedImages[fileName]; ok { + imageLock.RUnlock() + return img, nil + } + imageLock.RUnlock() - if _, ok := loadedImages[fileName]; !ok { - imgFile, err := fileutil.Open(filepath.Join(directory, fileName)) - if err != nil { - return nil, err - } - defer func() { - dlog.ErrorCheck(imgFile.Close()) - }() + imgFile, err := fileutil.Open(filepath.Join(directory, fileName)) + if err != nil { + return nil, err + } + defer func() { + dlog.ErrorCheck(imgFile.Close()) + }() - ext := filepath.Ext(fileName) - decoder, ok := fileDecoders[ext] - if !ok { - return nil, errors.New("No decoder available for file type: " + ext) - } - img, err := decoder(imgFile) + ext := filepath.Ext(fileName) + decoder, ok := fileDecoders[ext] + if !ok { + return nil, errors.New("No decoder available for file type: " + ext) + } + img, err := decoder(imgFile) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - // Todo: we internally just use *image.RGBA, but that choice - // of image encoding was arbitrary. If using the image.Image - // interface would not hurt performance considerably, we should - // just use that. - // - // This converts the - bounds := img.Bounds() - rgba := image.NewRGBA(bounds) - for x := 0; x < bounds.Max.X; x++ { - for y := 0; y < bounds.Max.Y; y++ { - rgba.Set(x, y, color.RGBAModel.Convert(img.At(x, y))) - } + // Todo: we internally just use *image.RGBA, but that choice + // of image encoding was arbitrary. If using the image.Image + // interface would not hurt performance considerably, we should + // just use that. + // + // This converts the + bounds := img.Bounds() + rgba := image.NewRGBA(bounds) + for x := 0; x < bounds.Max.X; x++ { + for y := 0; y < bounds.Max.Y; y++ { + rgba.Set(x, y, color.RGBAModel.Convert(img.At(x, y))) } + } - loadedImages[fileName] = rgba + imageLock.Lock() + loadedImages[fileName] = rgba + imageLock.Unlock() - dlog.Verb("Loaded filename: ", fileName) - } - return loadedImages[fileName], nil + dlog.Verb("Loaded filename: ", fileName) + return rgba, nil } // SpriteIsLoaded returns whether, when LoadSprite is called, a cached sheet will From 40884ca6fe0b2ea10ca2d11f2a61a5e405a5a078 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 17 Oct 2020 15:28:20 -0500 Subject: [PATCH 14/40] Add waitgroup to image batchload logic --- render/batchload.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/render/batchload.go b/render/batchload.go index bc18f464..ce71c3d3 100644 --- a/render/batchload.go +++ b/render/batchload.go @@ -7,6 +7,7 @@ import ( "regexp" "strconv" "strings" + "sync" "github.com/oakmound/oak/v2/dlog" "github.com/oakmound/oak/v2/fileutil" @@ -36,6 +37,8 @@ func BatchLoad(baseFolder string) error { warnFiles := []string{} + var wg sync.WaitGroup + for i, folder := range folders { dlog.Verb("folder ", i, folder.Name()) @@ -57,7 +60,9 @@ func BatchLoad(baseFolder string) error { if lower != name { warnFiles = append(warnFiles, relativePath) } + wg.Add(1) go func(baseFolder, relativePath string, possibleSheet bool, frameW, frameH int) { + defer wg.Done() buff, err := loadSprite(baseFolder, relativePath) if err != nil { dlog.Error(err) @@ -99,6 +104,7 @@ func BatchLoad(baseFolder string) error { dlog.Warn("The files", fileNames, "are not all lowercase. This may cause data to fail to load"+ " when using tools like go-bindata.") } + wg.Wait() return nil } From 9189b490c38ee45aace75d0baa20365f23b73d6c Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 21 Oct 2020 19:26:26 -0500 Subject: [PATCH 15/40] Start experimental os volume level support --- audio/audio.go | 15 +++++++++++++-- go.mod | 8 +++++--- go.sum | 11 +++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/audio/audio.go b/audio/audio.go index c42e30ec..56b81434 100644 --- a/audio/audio.go +++ b/audio/audio.go @@ -12,8 +12,9 @@ import ( // required to filter it through a sound font. type Audio struct { *font.Audio - toStop audio.Audio - X, Y *float64 + toStop audio.Audio + X, Y *float64 + setVolume int32 } // New returns an audio from a font, some audio data, and optional @@ -30,6 +31,15 @@ func New(f *font.Font, d Data, coords ...*float64) *Audio { return a } +// SetVolume attempts to set the volume of the underlying OS audio. +func (a *Audio) SetVolume(v int32) error { + a.setVolume = v + if a.toStop != nil { + return a.toStop.SetVolume(v) + } + return nil +} + // Play begin's an audio's playback func (a *Audio) Play() <-chan error { a2, err := a.Copy() @@ -45,6 +55,7 @@ func (a *Audio) Play() <-chan error { return errChannel(err) } a.toStop = a4 + a.toStop.SetVolume(a.setVolume) return a4.Play() } diff --git a/go.mod b/go.mod index 9421d4ef..83fb0d33 100644 --- a/go.mod +++ b/go.mod @@ -2,21 +2,23 @@ module github.com/oakmound/oak/v2 require ( github.com/200sc/go-dist v1.0.0 - github.com/200sc/klangsynthese v0.2.0 + github.com/200sc/klangsynthese v0.2.2-0.20201022002431-a0e14a8c862b github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect github.com/akavel/polyclip-go v0.0.0-20160111220610-2cfdb71461bd github.com/davecgh/go-spew v1.1.1 github.com/disintegration/gift v1.2.0 + github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d // indirect + github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/hajimehoshi/go-mp3 v0.2.1 // indirect + github.com/hajimehoshi/go-mp3 v0.3.1 // indirect github.com/oakmound/libudev v0.2.1 github.com/oakmound/shiny v0.4.2-beta.0.20200620232743-6df262b89055 github.com/oakmound/w32 v2.1.0+incompatible github.com/oov/directsound-go v0.0.0-20141101201356-e53e59c700bf // indirect github.com/pkg/errors v0.9.1 // indirect github.com/stretchr/testify v1.3.0 - github.com/yobert/alsa v0.0.0-20190412010934-e9392a2624b7 // indirect + github.com/yobert/alsa v0.0.0-20200618200352-d079056f5370 // indirect golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 diff --git a/go.sum b/go.sum index 2677b18f..0dbc61f2 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/200sc/go-dist v1.0.0 h1:Y9bl++4+d8NFv5hzwHyaxbXZmexVCic/rUdQOTq7Jec= github.com/200sc/go-dist v1.0.0/go.mod h1:+hhVw6j4BQF2HPwLFmsVM4VooSH9sn04apvH52hfboU= github.com/200sc/klangsynthese v0.2.0 h1:kVveiDyAXOylXJLsQKiT1wXD6Yy+jewrdrfB3urZC6c= github.com/200sc/klangsynthese v0.2.0/go.mod h1:1yEA6LmYbsEoAPJx+JN+UQk7k5yXebfWsuM1yGPK/3s= +github.com/200sc/klangsynthese v0.2.2-0.20201022002431-a0e14a8c862b h1:6Gk8u6eHdE6Xcn3t3BDei1e83FUUw6eH/LhOiDECHr0= +github.com/200sc/klangsynthese v0.2.2-0.20201022002431-a0e14a8c862b/go.mod h1:1yEA6LmYbsEoAPJx+JN+UQk7k5yXebfWsuM1yGPK/3s= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= @@ -16,13 +18,20 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/gift v1.2.0 h1:VMQeei2F+ZtsHjMgP6Sdt1kFjRhs2lGz8ljEOPeIR50= github.com/disintegration/gift v1.2.0/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d h1:HB5J9+f1xpkYLgWQ/RqEcbp3SEufyOIMYLoyKNKiG7E= +github.com/eaburns/bit v0.0.0-20131029213740-7bd5cd37375d/go.mod h1:CHkHWWZ4kbGY6jEy1+qlitDaCtRgNvCOQdakj/1Yl/Q= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1 h1:wl/ggSfTHqAy46hyzw1IlrUYwjqmXYvbJyPdH3rT7YE= +github.com/eaburns/flac v0.0.0-20171003200620-9a6fb92396d1/go.mod h1:frG94byMNy+1CgGrQ25dZ+17tf98EN+OYBQL4Zh612M= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8= github.com/hajimehoshi/go-mp3 v0.2.1 h1:DH4ns3cPv39n3cs8MPcAlWqPeAwLCK8iNgqvg0QBWI8= github.com/hajimehoshi/go-mp3 v0.2.1/go.mod h1:Rr+2P46iH6PwTPVgSsEwBkon0CK5DxCAeX/Rp65DCTE= github.com/hajimehoshi/go-mp3 v0.2.1/go.mod h1:Rr+2P46iH6PwTPVgSsEwBkon0CK5DxCAeX/Rp65DCTE= +github.com/hajimehoshi/go-mp3 v0.3.1 h1:pn/SKU1+/rfK8KaZXdGEC2G/KCB2aLRjbTCrwKcokao= +github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.3.4/go.mod h1:PgjqsBJff0efqL2nlMJidJgVJywLn6M4y8PI4TfeWfA= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= github.com/oakmound/libudev v0.2.1 h1:gaXuw7Pbt3RSRxbUakAjl0dSW6Wo3TZWpwS5aMq8+EA= github.com/oakmound/libudev v0.2.1/go.mod h1:zYF5CkHY+UP6lzWbPR+XoVAscl/s+OncWA//qWjMLUs= github.com/oakmound/shiny v0.4.2-0.20191119030223-0bee53b02481 h1:CRXgFNZ3V76GAjgwZ6c6V/PmBFy9iX55sC+fIDBpTVs= @@ -47,6 +56,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/yobert/alsa v0.0.0-20190412010934-e9392a2624b7 h1:a2YSmAxh3Lf0hRrF6IVP2iBJDkTXig4baBsluAgWenk= github.com/yobert/alsa v0.0.0-20190412010934-e9392a2624b7/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw= +github.com/yobert/alsa v0.0.0-20200618200352-d079056f5370 h1:I8PHpJWTMTJZVDoosy8aXslFGe7wvcUbol7fOrVy4Tc= +github.com/yobert/alsa v0.0.0-20200618200352-d079056f5370/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= From 2456f0bdf60b7b296b68e1fbbb845ce83f299e6c Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 7 Nov 2020 10:42:17 -0600 Subject: [PATCH 16/40] Accept stringers and string pointers for buttons --- entities/x/btn/button.go | 11 +++++++++++ entities/x/btn/textOptions.go | 26 ++++++++++++++++++++++++++ render/text.go | 11 +++++++++++ 3 files changed, 48 insertions(+) diff --git a/entities/x/btn/button.go b/entities/x/btn/button.go index b6ccad77..ce423e99 100644 --- a/entities/x/btn/button.go +++ b/entities/x/btn/button.go @@ -1,6 +1,7 @@ package btn import ( + "fmt" "image/color" "strconv" "strings" @@ -30,6 +31,8 @@ type Generator struct { Font *render.Font Layers []int Text string + TextPtr *string + TextStringer fmt.Stringer Children []Generator Bindings map[string][]event.Bindable Trigger string @@ -137,6 +140,14 @@ func (g Generator) generate(parent *Generator) Btn { txtbx := NewTextBox(g.Cid, g.X, g.Y, g.W, g.H, g.TxtX, g.TxtY, font, box, g.Layers...) txtbx.SetString(g.Text) btn = txtbx + } else if g.TextPtr != nil { + txtbx := NewTextBox(g.Cid, g.X, g.Y, g.W, g.H, g.TxtX, g.TxtY, font, box, g.Layers...) + txtbx.SetStringPtr(g.TextPtr) + btn = txtbx + } else if g.TextStringer != nil { + txtbx := NewTextBox(g.Cid, g.X, g.Y, g.W, g.H, g.TxtX, g.TxtY, font, box, g.Layers...) + txtbx.SetStringer(g.TextStringer) + btn = txtbx } else { btn = NewBox(g.Cid, g.X, g.Y, g.W, g.H, box, g.Layers...) } diff --git a/entities/x/btn/textOptions.go b/entities/x/btn/textOptions.go index 4e13dc3e..7c06f5c1 100644 --- a/entities/x/btn/textOptions.go +++ b/entities/x/btn/textOptions.go @@ -1,17 +1,43 @@ package btn import ( + "fmt" + "github.com/oakmound/oak/v2/render" ) //Text sets the text of the button to be generated func Text(s string) Option { return func(g Generator) Generator { + g.TextPtr = nil + g.TextStringer = nil g.Text = s return g } } +// TextPtr sets the text of the button to be generated +// to a string pointer. +func TextPtr(s *string) Option { + return func(g Generator) Generator { + g.Text = "" + g.TextStringer = nil + g.TextPtr = s + return g + } +} + +// TextStringer sets the text of the generated button to +// use a fmt.Stringer String call +func TextStringer(s fmt.Stringer) Option { + return func(g Generator) Generator { + g.Text = "" + g.TextPtr = nil + g.TextStringer = s + return g + } +} + //Font sets the font for the text of the button to be generated func Font(f *render.Font) Option { return func(g Generator) Generator { diff --git a/render/text.go b/render/text.go index e5e4fd6d..f84db060 100644 --- a/render/text.go +++ b/render/text.go @@ -108,6 +108,16 @@ func (t *Text) SetString(str string) { t.text = stringStringer(str) } +// SetStringPtr accepts a string pointer as the stringer to be written +func (t *Text) SetStringPtr(str *string) { + t.text = stringPtrStringer{str} +} + +// SetStringer accepts an fmt.Stringer to write +func (t *Text) SetStringer(s fmt.Stringer) { + t.text = s +} + //SetInt takes and converts the input integer to a string to write func (t *Text) SetInt(i int) { t.text = stringStringer(strconv.Itoa(i)) @@ -115,6 +125,7 @@ func (t *Text) SetInt(i int) { // SetIntP takes in an integer pointer that will be drawn at whatever // the value is behind the pointer when it is drawn +// TODO: (3.0) rename to SetIntPtr func (t *Text) SetIntP(i *int) { t.text = stringerIntPointer{i} } From bb120cc6177d77d73ed3e834fe90ae159cc36184 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Mon, 16 Nov 2020 18:29:02 -0600 Subject: [PATCH 17/40] Add SetTitle and Titler as top level screen options --- screenOpts.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/screenOpts.go b/screenOpts.go index 19ebaa62..150cd6cb 100644 --- a/screenOpts.go +++ b/screenOpts.go @@ -68,3 +68,18 @@ func SetTopMost(on bool) error { Operation: "SetTopMost", } } + +type Titler interface { + SetTitle(string) error +} + +// SetTopMost attempts to set the local oak window to stay on top of other windows. +// If the window does not support this functionality, it will report as such. +func SetTitle(title string) error { + if t, ok := windowControl.(Titler); ok { + return t.SetTitle(title) + } + return oakerr.UnsupportedPlatform{ + Operation: "SetTitle", + } +} From 42aafe7e7e138fc0f3f23ad7affd436d651d0de6 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 5 Dec 2020 11:57:21 -0600 Subject: [PATCH 18/40] Add support for blanking out audio on startup, to allow faster development iteration This feature replaces all audio loaded at startup with empty bytes, not opening or loading the files in question. This improves startup times for games with long music tracks, and is meant to be used when iterating bug fixes of non-audio components. --- audio/load.go | 36 ++++++++++++++++++++++++++++++++++- config.go | 52 +++++++++++++++++++++++++++++++++------------------ loading.go | 7 ++++++- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/audio/load.go b/audio/load.go index 097b8754..9e10cf16 100644 --- a/audio/load.go +++ b/audio/load.go @@ -108,7 +108,17 @@ func setLoaded(filename string, data Data) { // BatchLoad attempts to load all files within a given directory // depending on their file ending (currently supporting .wav and .mp3) func BatchLoad(baseFolder string) error { + return batchLoad(baseFolder, false) +} + +// BlankBatchLoad acts like BatchLoad, but replaces all loaded assets +// with empty audio constructs. This is intended to reduce start-up +// times in development. +func BlankBatchLoad(baseFolder string) error { + return batchLoad(baseFolder, true) +} +func batchLoad(baseFolder string, blankOut bool) error { files, err := fileutil.ReadDir(baseFolder) if err != nil { @@ -125,7 +135,13 @@ func BatchLoad(baseFolder string) error { case ".wav", ".mp3": dlog.Verb("loading file ", n) eg.Go(func() error { - _, err := Load(baseFolder, n) + var err error + if blankOut { + dlog.Verb("blank loading file") + err = blankLoad(n) + } else { + _, err = Load(baseFolder, n) + } if err != nil { dlog.Error(err) return err @@ -141,3 +157,21 @@ func BatchLoad(baseFolder string) error { dlog.Verb("Loading complete") return err } + +func blankLoad(filename string) error { + mformat := audio.Format{ + SampleRate: 44000, + Bits: 16, + Channels: 2, + } + buffer, err := audio.EncodeBytes( + audio.Encoding{ + Format: mformat, + }) + if err != nil { + return err + } + data := buffer.(audio.FullAudio) + setLoaded(filename, data) + return nil +} diff --git a/config.go b/config.go index f8e71f1e..e1d73cc8 100644 --- a/config.go +++ b/config.go @@ -34,10 +34,13 @@ var ( // These are the default settings of a project. Anything within SetupConfig // that is set to its zero value will not overwrite these settings. conf = Config{ - Assets: Assets{"assets/", "audio/", "images/", "font/"}, - Debug: Debug{"", "ERROR"}, - Screen: Screen{0, 0, 480, 640, 1, 0, 0}, - Font: Font{"none", 12.0, 72.0, "", "white"}, + Assets: Assets{"assets/", "audio/", "images/", "font/"}, + Debug: Debug{"", "ERROR"}, + Screen: Screen{0, 0, 480, 640, 1, 0, 0}, + Font: Font{"none", 12.0, 72.0, "", "white"}, + BatchLoadOptions: BatchLoadOptions{ + BlankOutAudio: true, + }, FrameRate: 60, DrawFrameRate: 60, Language: "English", @@ -51,18 +54,19 @@ var ( // Config stores initialization settings for oak. type Config struct { - Assets Assets `json:"assets"` - Debug Debug `json:"debug"` - Screen Screen `json:"screen"` - Font Font `json:"font"` - FrameRate int `json:"frameRate"` - DrawFrameRate int `json:"drawFrameRate"` - Language string `json:"language"` - Title string `json:"title"` - BatchLoad bool `json:"batchLoad"` - GestureSupport bool `json:"gestureSupport"` - LoadBuiltinCommands bool `json:"loadBuiltinCommands"` - TrackInputChanges bool `json:"trackInputChanges"` + Assets Assets `json:"assets"` + Debug Debug `json:"debug"` + Screen Screen `json:"screen"` + Font Font `json:"font"` + BatchLoadOptions BatchLoadOptions `json:"batchLoadOptions` + FrameRate int `json:"frameRate"` + DrawFrameRate int `json:"drawFrameRate"` + Language string `json:"language"` + Title string `json:"title"` + BatchLoad bool `json:"batchLoad"` + GestureSupport bool `json:"gestureSupport"` + LoadBuiltinCommands bool `json:"loadBuiltinCommands"` + TrackInputChanges bool `json:"trackInputChanges"` } // Assets is a json type storing paths to different asset folders @@ -89,8 +93,8 @@ type Screen struct { // Target sets the expected dimensions of the monitor the game will be opened on, in pixels. // If Fullscreen is false, then a scaling will be applied to correct the game screen size to be // appropriate for the Target size. If no TargetWidth or Height is provided, scaling will not - // be adjusted. - TargetWidth int `json:"targetHeight"` + // be adjusted. + TargetWidth int `json:"targetHeight"` TargetHeight int `json:"targetWidth"` } @@ -103,6 +107,12 @@ type Font struct { Color string `json:"color"` } +// BatchLoadOptions is a json type storing customizations for batch loading. +// These settings do not take effect unless batch load is true. +type BatchLoadOptions struct { + BlankOutAudio bool `json:"blankOutAudio"` +} + // LoadConf loads a config file, that could exist inside // oak's binary data storage (see fileutil), to SetupConfig func LoadConf(filePath string) error { @@ -185,6 +195,10 @@ func initConfFont() { } } +func initConfBatchLoad() { + conf.BatchLoadOptions.BlankOutAudio = SetupConfig.BatchLoadOptions.BlankOutAudio +} + func initConf() { initConfAssets() @@ -195,6 +209,8 @@ func initConf() { initConfFont() + initConfBatchLoad() + if SetupConfig.FrameRate != 0 { conf.FrameRate = SetupConfig.FrameRate } diff --git a/loading.go b/loading.go index 098f6e0e..fab7da78 100644 --- a/loading.go +++ b/loading.go @@ -27,7 +27,12 @@ func loadAssets(imageDir, audioDir string) { return nil }) eg.Go(func() error { - err := audio.BatchLoad(audioDir) + var err error + if conf.BatchLoadOptions.BlankOutAudio { + err = audio.BlankBatchLoad(audioDir) + } else { + err = audio.BatchLoad(audioDir) + } if err != nil { return err } From c68576ba2a8756264c55eeaacee52621bcc835a8 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 5 Dec 2020 12:05:08 -0600 Subject: [PATCH 19/40] Remove four character requirement from image decoders This was an artifact from before using filepath.Ext. All lengths are properly supported now. --- render/decoder.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/render/decoder.go b/render/decoder.go index 96142ca8..0a8a2a01 100644 --- a/render/decoder.go +++ b/render/decoder.go @@ -19,11 +19,11 @@ type Decoder func(io.Reader) (image.Image, error) var ( fileDecoders = map[string]Decoder{ - "jpeg": jpeg.Decode, - ".jpg": jpeg.Decode, - ".gif": gif.Decode, - ".png": png.Decode, - ".bmp": bmp.Decode, + ".jpeg": jpeg.Decode, + ".jpg": jpeg.Decode, + ".gif": gif.Decode, + ".png": png.Decode, + ".bmp": bmp.Decode, } ) @@ -31,12 +31,16 @@ var ( // for file loading. If the extension string is already set, // the existing decoder will not be overwritten. func RegisterDecoder(ext string, decoder Decoder) error { - if len(ext) != 4 { - return oakerr.InvalidLength{ - InputName: "ext", - RequiredLength: 4, - Length: len(ext), + _, ok := fileDecoders[ext] + if ok { + return oakerr.ExistingElement{ + InputName: "ext", + InputType: "string", + Overwritten: false, + } } + fileDecoders[ext] = decoder + return nil } _, ok := fileDecoders[ext] if ok { From fd1fd449ad454b0914fbbe426a7b6da3561063f4 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 5 Dec 2020 12:26:45 -0600 Subject: [PATCH 20/40] Add the ability to blank out large images on batch load --- config.go | 12 +++++++++--- loading.go | 2 +- render/batchload.go | 7 ++++++- render/decoder.go | 22 ++++++++++++++++++---- render/loader_test.go | 2 +- render/loadsheet.go | 2 +- render/loadsprite.go | 35 ++++++++++++++++++++++++++++++++--- 7 files changed, 68 insertions(+), 14 deletions(-) diff --git a/config.go b/config.go index e1d73cc8..c3e48112 100644 --- a/config.go +++ b/config.go @@ -39,7 +39,8 @@ var ( Screen: Screen{0, 0, 480, 640, 1, 0, 0}, Font: Font{"none", 12.0, 72.0, "", "white"}, BatchLoadOptions: BatchLoadOptions{ - BlankOutAudio: true, + BlankOutAudio: false, + MaxImageFileSize: 0, }, FrameRate: 60, DrawFrameRate: 60, @@ -58,7 +59,7 @@ type Config struct { Debug Debug `json:"debug"` Screen Screen `json:"screen"` Font Font `json:"font"` - BatchLoadOptions BatchLoadOptions `json:"batchLoadOptions` + BatchLoadOptions BatchLoadOptions `json:"batchLoadOptions"` FrameRate int `json:"frameRate"` DrawFrameRate int `json:"drawFrameRate"` Language string `json:"language"` @@ -110,7 +111,8 @@ type Font struct { // BatchLoadOptions is a json type storing customizations for batch loading. // These settings do not take effect unless batch load is true. type BatchLoadOptions struct { - BlankOutAudio bool `json:"blankOutAudio"` + BlankOutAudio bool `json:"blankOutAudio"` + MaxImageFileSize int64 `json:"maxImageFileSize"` } // LoadConf loads a config file, that could exist inside @@ -197,6 +199,10 @@ func initConfFont() { func initConfBatchLoad() { conf.BatchLoadOptions.BlankOutAudio = SetupConfig.BatchLoadOptions.BlankOutAudio + + if SetupConfig.BatchLoadOptions.MaxImageFileSize != 0 { + conf.BatchLoadOptions.MaxImageFileSize = SetupConfig.BatchLoadOptions.MaxImageFileSize + } } func initConf() { diff --git a/loading.go b/loading.go index fab7da78..8c8a1962 100644 --- a/loading.go +++ b/loading.go @@ -19,7 +19,7 @@ func loadAssets(imageDir, audioDir string) { dlog.Info("Loading Images") var eg errgroup.Group eg.Go(func() error { - err := render.BatchLoad(imageDir) + err := render.BlankBatchLoad(imageDir, conf.BatchLoadOptions.MaxImageFileSize) if err != nil { return err } diff --git a/render/batchload.go b/render/batchload.go index ce71c3d3..d6e07599 100644 --- a/render/batchload.go +++ b/render/batchload.go @@ -27,7 +27,12 @@ var ( // represent, so a "tiles": "32" field in the json would indicate that sprite sheets // in the /tiles folder should be read as 32x32 func BatchLoad(baseFolder string) error { + return BlankBatchLoad(baseFolder, 0) +} +// BlankBatchLoad acts like BatchLoad, but will not load and instead return a blank image +// of the appropriate dimensions for anything above maxFileSize. +func BlankBatchLoad(baseFolder string, maxFileSize int64) error { folders, err := fileutil.ReadDir(baseFolder) if err != nil { dlog.Error(err) @@ -63,7 +68,7 @@ func BatchLoad(baseFolder string) error { wg.Add(1) go func(baseFolder, relativePath string, possibleSheet bool, frameW, frameH int) { defer wg.Done() - buff, err := loadSprite(baseFolder, relativePath) + buff, err := loadSprite(baseFolder, relativePath, maxFileSize) if err != nil { dlog.Error(err) return diff --git a/render/decoder.go b/render/decoder.go index 0a8a2a01..47ca8d83 100644 --- a/render/decoder.go +++ b/render/decoder.go @@ -17,6 +17,10 @@ import ( // be an image file. type Decoder func(io.Reader) (image.Image, error) +// CfgDecoder is an equivalent to Decoder that just exports +// the color model and dimensions of the image. +type CfgDecoder func(io.Reader) (image.Config, error) + var ( fileDecoders = map[string]Decoder{ ".jpeg": jpeg.Decode, @@ -25,6 +29,13 @@ var ( ".png": png.Decode, ".bmp": bmp.Decode, } + cfgDecoders = map[string]CfgDecoder{ + ".jpeg": jpeg.DecodeConfig, + ".jpg": jpeg.DecodeConfig, + ".gif": gif.DecodeConfig, + ".png": png.DecodeConfig, + ".bmp": bmp.DecodeConfig, + } ) // RegisterDecoder adds a decoder to the set of image decoders @@ -38,11 +49,14 @@ func RegisterDecoder(ext string, decoder Decoder) error { InputType: "string", Overwritten: false, } - } + } fileDecoders[ext] = decoder return nil - } - _, ok := fileDecoders[ext] +} + +// RegisterCfgDecoder acts like RegisterDecoder for CfgDecoders +func RegisterCfgDecoder(ext string, decoder CfgDecoder) error { + _, ok := cfgDecoders[ext] if ok { return oakerr.ExistingElement{ InputName: "ext", @@ -50,6 +64,6 @@ func RegisterDecoder(ext string, decoder Decoder) error { Overwritten: false, } } - fileDecoders[ext] = decoder + cfgDecoders[ext] = decoder return nil } diff --git a/render/loader_test.go b/render/loader_test.go index a6273026..ab609d07 100644 --- a/render/loader_test.go +++ b/render/loader_test.go @@ -20,7 +20,7 @@ func TestBatchLoad(t *testing.T) { sh, err := GetSheet(imgPath1) assert.Nil(t, err) assert.Equal(t, len(sh.ToSprites()), 8) - _, err = loadSprite("dir", "dummy.jpg") + _, err = loadSprite("dir", "dummy.jpg", 0) assert.NotNil(t, err) sp, err := GetSprite("dummy.gif") assert.Nil(t, sp) diff --git a/render/loadsheet.go b/render/loadsheet.go index 3a9d9144..dba6d125 100644 --- a/render/loadsheet.go +++ b/render/loadsheet.go @@ -31,7 +31,7 @@ func LoadSheet(directory, fileName string, w, h, pad int) (*Sheet, error) { if !ok { dlog.Verb("Missing file in loaded images: ", fileName) - rgba, err = loadSprite(directory, fileName) + rgba, err = loadSprite(directory, fileName, 0) if err != nil { return nil, err } diff --git a/render/loadsprite.go b/render/loadsprite.go index f3c9e850..9b99e7d3 100644 --- a/render/loadsprite.go +++ b/render/loadsprite.go @@ -4,6 +4,7 @@ import ( "errors" "image" "image/color" + "os" "path/filepath" "github.com/oakmound/oak/v2/dlog" @@ -11,7 +12,7 @@ import ( "github.com/oakmound/oak/v2/oakerr" ) -func loadSprite(directory, fileName string) (*image.RGBA, error) { +func loadSprite(directory, fileName string, maxFileSize int64) (*image.RGBA, error) { imageLock.RLock() if img, ok := loadedImages[fileName]; ok { @@ -20,7 +21,9 @@ func loadSprite(directory, fileName string) (*image.RGBA, error) { } imageLock.RUnlock() - imgFile, err := fileutil.Open(filepath.Join(directory, fileName)) + fullPath := filepath.Join(directory, fileName) + + imgFile, err := fileutil.Open(fullPath) if err != nil { return nil, err } @@ -29,6 +32,32 @@ func loadSprite(directory, fileName string) (*image.RGBA, error) { }() ext := filepath.Ext(fileName) + + cfgDecoder, ok := cfgDecoders[ext] + if maxFileSize != 0 && ok { + info, err := os.Lstat(fullPath) + // This can't reasonably error as we already loaded the file above + dlog.ErrorCheck(err) + if info.Size() > maxFileSize { + // construct a blank image of the correct dimensions + cfg, err := cfgDecoder(imgFile) + if err != nil { + return nil, err + } + bounds := image.Rectangle{ + Min: image.Point{0, 0}, + Max: image.Point{cfg.Width, cfg.Height}, + } + rgba := image.NewRGBA(bounds) + imageLock.Lock() + loadedImages[fileName] = rgba + imageLock.Unlock() + + dlog.Verb("Blank loaded filename: ", fileName) + return rgba, nil + } + } + decoder, ok := fileDecoders[ext] if !ok { return nil, errors.New("No decoder available for file type: " + ext) @@ -90,7 +119,7 @@ func LoadSprite(directory, fileName string) (*Sprite, error) { if directory == "" { directory = dir } - r, err := loadSprite(directory, fileName) + r, err := loadSprite(directory, fileName, 0) if err != nil { dlog.Error(err) return nil, err From dc4c7872b3f349be526d7c77a6aa06a221132863 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 20 Jan 2021 20:27:16 -0600 Subject: [PATCH 21/40] Upgrade x/image library for font changes --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 83fb0d33..059d8e0e 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/stretchr/testify v1.3.0 github.com/yobert/alsa v0.0.0-20200618200352-d079056f5370 // indirect - golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 + golang.org/x/image v0.0.0-20201208152932-35266b937fa6 golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 ) diff --git a/go.sum b/go.sum index 0dbc61f2..c55c348c 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/yobert/alsa v0.0.0-20200618200352-d079056f5370/go.mod h1:CaowXBWOiSGW golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mobile v0.0.0-20190307202846-d2e1c1c4a691/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= From 7650ed5aeb3b437fdc7b9c7b4d3bb79a9995b146 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sun, 24 Jan 2021 14:13:28 -0600 Subject: [PATCH 22/40] Fix a goroutine leak in event.Bus.ResolvePending --- event/resolve.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/event/resolve.go b/event/resolve.go index 3ea897ac..1ebec40c 100644 --- a/event/resolve.go +++ b/event/resolve.go @@ -2,14 +2,16 @@ package event // ResolvePending is a contant loop that tracks slices of bind or unbind calls // and resolves them individually such that they don't break the bus -// Todo: this should be a function on the event bus itself, and should have a better name +// Todo: this should have a better name // If you ask "Why does this not use select over channels, share memory by communicating", // the answer is we tried, and it was cripplingly slow. func (eb *Bus) ResolvePending() { eb.init.Do(func() { - for { - eb.Flush() - } + go func() { + for { + eb.Flush() + } + }() }) } From cdb28cf4335fa829b9ab040554c34bf3ac76f641 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sun, 24 Jan 2021 14:21:23 -0600 Subject: [PATCH 23/40] Update decoder tests for more lenient extension registration --- render/decoder_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/render/decoder_test.go b/render/decoder_test.go index 580b9459..3d08cec0 100644 --- a/render/decoder_test.go +++ b/render/decoder_test.go @@ -7,9 +7,6 @@ import ( ) func TestRegisterDecoder(t *testing.T) { - assert.NotNil(t, RegisterDecoder("too long", nil)) - short := "s" - assert.NotNil(t, RegisterDecoder(short, nil)) assert.NotNil(t, RegisterDecoder(".png", nil)) assert.Nil(t, RegisterDecoder(".new", nil)) } From ff0fefda28eea7f2386942e8fcaf1b5ae553e656 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sun, 31 Jan 2021 23:16:08 -0600 Subject: [PATCH 24/40] Add font safety to Text.ToSprite --- render/text.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/render/text.go b/render/text.go index f84db060..ee330903 100644 --- a/render/text.go +++ b/render/text.go @@ -65,16 +65,18 @@ func (f *Font) NewStrPtrText(str *string, x, y float64) *Text { // DrawOffset for a text object draws the text at t.(X,Y) + (xOff,yOff) func (t *Text) DrawOffset(buff draw.Image, xOff, yOff float64) { - t.d.Drawer.Dst = buff - t.d.Drawer.Dot = fixed.P(int(t.X()+xOff), int(t.Y()+yOff)) - t.d.DrawString(t.text.String()) + t.drawWithFont(buff, xOff, yOff, t.d) +} + +func (t *Text) drawWithFont(buff draw.Image, xOff, yOff float64, fnt *Font) { + fnt.Drawer.Dst = buff + fnt.Drawer.Dot = fixed.P(int(t.X()+xOff), int(t.Y()+yOff)) + fnt.DrawString(t.text.String()) } // Draw for a text draws the text at its layeredPoint position func (t *Text) Draw(buff draw.Image) { - t.d.Drawer.Dst = buff - t.d.Drawer.Dot = fixed.P(int(t.X()), int(t.Y())) - t.d.DrawString(t.text.String()) + t.drawWithFont(buff, 0, 0, t.d) } // SetFont sets the drawer which renders the text each frame @@ -171,9 +173,10 @@ func (t *Text) Wrap(charLimit int, vertInc float64) []*Text { // modifiable in terms of its text content, but is modifiable in terms // of mod.Transform or mod.Filter. func (t *Text) ToSprite() *Sprite { - width := t.d.MeasureString(t.text.String()).Round() - height := t.d.bounds.Max.Y() + tmpFnt := t.d.Copy() + width := tmpFnt.MeasureString(t.text.String()).Round() + height := tmpFnt.bounds.Max.Y() s := NewEmptySprite(t.X(), t.Y()-float64(height), width, height+5) - t.DrawOffset(s.GetRGBA(), -t.X(), -t.Y()+float64(height)) + t.drawWithFont(s.GetRGBA(), -t.X(), -t.Y()+float64(height), tmpFnt) return s } From b249d9f44e8e9c4beba26b892811735aa9d7873e Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 20 Feb 2021 09:26:21 -0600 Subject: [PATCH 25/40] Add EventRefreshRate config option --- config.go | 7 +++++++ event/bus.go | 2 ++ event/handler.go | 9 +++++++++ event/resolve.go | 7 +++++++ init.go | 8 +++++++- 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/config.go b/config.go index c3e48112..1c601b11 100644 --- a/config.go +++ b/config.go @@ -3,6 +3,7 @@ package oak import ( "encoding/json" "io" + "time" "github.com/oakmound/oak/v2/fileutil" @@ -50,6 +51,7 @@ var ( GestureSupport: false, LoadBuiltinCommands: false, TrackInputChanges: false, + EventRefreshRate: 0 * time.Second, } ) @@ -64,6 +66,7 @@ type Config struct { DrawFrameRate int `json:"drawFrameRate"` Language string `json:"language"` Title string `json:"title"` + EventRefreshRate time.Duration `json:"refreshRate"` BatchLoad bool `json:"batchLoad"` GestureSupport bool `json:"gestureSupport"` LoadBuiltinCommands bool `json:"loadBuiltinCommands"` @@ -233,6 +236,10 @@ func initConf() { conf.Title = SetupConfig.Title } + if SetupConfig.EventRefreshRate != 0 { + conf.EventRefreshRate = SetupConfig.EventRefreshRate + } + conf.BatchLoad = SetupConfig.BatchLoad conf.GestureSupport = SetupConfig.GestureSupport conf.LoadBuiltinCommands = SetupConfig.LoadBuiltinCommands diff --git a/event/bus.go b/event/bus.go index 0fcf6d04..1c30bfa6 100644 --- a/event/bus.go +++ b/event/bus.go @@ -3,6 +3,7 @@ package event import ( "reflect" "sync" + "time" "github.com/oakmound/oak/v2/timing" ) @@ -58,6 +59,7 @@ type Bus struct { unbinds []binding unbindAllAndRebinds []UnbindAllOption framerate int + refreshRate time.Duration mutex sync.RWMutex pendingMutex sync.Mutex diff --git a/event/handler.go b/event/handler.go index a25cdaf8..6c9aaa78 100644 --- a/event/handler.go +++ b/event/handler.go @@ -32,6 +32,15 @@ type FullHandler interface { TriggerBack(event string, data interface{}) chan bool } +// ConfigHandler contains methods for configuring a running handler. +// ConfigHandler is a new interface for backwards compatability. +// TODO: v3: compress this interface into the main handler +type ConfigHandler interface { + // SetRefreshRate configures how often a running handler will check + // for new binding and unbinding requests. + SetRefreshRate(time.Duration) +} + // A Pauser is a handler that can be paused. type Pauser interface { Pause() diff --git a/event/resolve.go b/event/resolve.go index 1ebec40c..c2aba104 100644 --- a/event/resolve.go +++ b/event/resolve.go @@ -1,5 +1,7 @@ package event +import "time" + // ResolvePending is a contant loop that tracks slices of bind or unbind calls // and resolves them individually such that they don't break the bus // Todo: this should have a better name @@ -9,12 +11,17 @@ func (eb *Bus) ResolvePending() { eb.init.Do(func() { go func() { for { + time.Sleep(eb.refreshRate) eb.Flush() } }() }) } +func (eb *Bus) SetRefreshRate(refreshRate time.Duration) { + eb.refreshRate = refreshRate +} + func (eb *Bus) resolveUnbindAllAndRebinds() { eb.mutex.Lock() eb.pendingMutex.Lock() diff --git a/init.go b/init.go index fe95da4e..17daa264 100644 --- a/init.go +++ b/init.go @@ -5,9 +5,10 @@ import ( "os" "path/filepath" - "github.com/oakmound/shiny/driver" "github.com/oakmound/oak/v2/dlog" + "github.com/oakmound/oak/v2/event" "github.com/oakmound/oak/v2/render" + "github.com/oakmound/shiny/driver" ) var ( @@ -109,6 +110,11 @@ func Init(firstScene string) { if conf.TrackInputChanges { trackJoystickChanges() } + if conf.EventRefreshRate != 0 { + if cfgHandler, ok := logicHandler.(event.ConfigHandler); ok { + cfgHandler.SetRefreshRate(conf.EventRefreshRate) + } + } // END of loading variables from configuration SeedRNG(DefaultSeed) From 9f9783fac4aa1e30f616091e364b58c3b67ebd14 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 20 Feb 2021 09:35:40 -0600 Subject: [PATCH 26/40] Add DisableDebugConsole config option --- config.go | 3 +++ init.go | 8 +++++--- sceneLoop.go | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/config.go b/config.go index 1c601b11..8e793a5b 100644 --- a/config.go +++ b/config.go @@ -52,6 +52,7 @@ var ( LoadBuiltinCommands: false, TrackInputChanges: false, EventRefreshRate: 0 * time.Second, + DisableDebugConsole: false, } ) @@ -71,6 +72,7 @@ type Config struct { GestureSupport bool `json:"gestureSupport"` LoadBuiltinCommands bool `json:"loadBuiltinCommands"` TrackInputChanges bool `json:"trackInputChanges"` + DisableDebugConsole bool `json:"disableDebugConsole"` } // Assets is a json type storing paths to different asset folders @@ -244,6 +246,7 @@ func initConf() { conf.GestureSupport = SetupConfig.GestureSupport conf.LoadBuiltinCommands = SetupConfig.LoadBuiltinCommands conf.TrackInputChanges = SetupConfig.TrackInputChanges + conf.DisableDebugConsole = SetupConfig.DisableDebugConsole dlog.Error(conf) } diff --git a/init.go b/init.go index 17daa264..bfff0755 100644 --- a/init.go +++ b/init.go @@ -127,12 +127,14 @@ func Init(firstScene string) { conf.Assets.AudioPath) dlog.Info("Init Scene Loop") - go sceneLoop(firstScene, conf.TrackInputChanges) + go sceneLoop(firstScene, conf.TrackInputChanges, conf.DisableDebugConsole) dlog.Info("Init asset load") render.SetAssetPaths(imageDir) go loadAssets(imageDir, audioDir) - dlog.Info("Init Console") - go debugConsole(debugResetCh, skipSceneCh, os.Stdin) + if !conf.DisableDebugConsole { + dlog.Info("Init Console") + go debugConsole(debugResetCh, skipSceneCh, os.Stdin) + } dlog.Info("Init Main Driver") InitDriver(lifecycleLoop) } diff --git a/sceneLoop.go b/sceneLoop.go index 55238611..64c3836f 100644 --- a/sceneLoop.go +++ b/sceneLoop.go @@ -34,7 +34,7 @@ var ( var firstScene string -func sceneLoop(first string, trackingInputs bool) { +func sceneLoop(first string, trackingInputs bool, debugConsoleDisabled bool) { var prevScene string result := new(scene.Result) @@ -140,7 +140,7 @@ func sceneLoop(first string, trackingInputs bool) { result = new(scene.Result) } - if !debugResetInProgress { + if !debugConsoleDisabled && !debugResetInProgress { debugResetInProgress = true go func() { debugResetCh <- true From 1c6ce24e4b3c1e100d8f3b83155ea483e3ef73a5 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Sat, 20 Feb 2021 10:10:18 -0600 Subject: [PATCH 27/40] Remove incorrect comment from mod.CutShape --- render/mod/cut.go | 1 - 1 file changed, 1 deletion(-) diff --git a/render/mod/cut.go b/render/mod/cut.go index a745866c..be7f506d 100644 --- a/render/mod/cut.go +++ b/render/mod/cut.go @@ -73,7 +73,6 @@ func CutShape(sh shape.Shape) Mod { newRgba := image.NewRGBA(bds) newRect := sh.Rect(bds.Dx(), bds.Dy()) - // start off as a copy for x := bds.Min.X; x < bds.Max.X; x++ { for y := bds.Min.Y; y < bds.Max.Y; y++ { if newRect[x-bds.Min.X][y-bds.Min.Y] { From 4e4a98b681ced02e431be95f365e0fbd654ff9e5 Mon Sep 17 00:00:00 2001 From: implausiblyfun Date: Mon, 22 Feb 2021 19:17:34 -0500 Subject: [PATCH 28/40] chore: comment updates for 2.4.1 exported fxns. --- event/resolve.go | 6 ++++-- render/particle/source.go | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/event/resolve.go b/event/resolve.go index c2aba104..1c8ca6d9 100644 --- a/event/resolve.go +++ b/event/resolve.go @@ -2,8 +2,9 @@ package event import "time" -// ResolvePending is a contant loop that tracks slices of bind or unbind calls -// and resolves them individually such that they don't break the bus +// ResolvePending is a constant loop that tracks slices of bind or unbind calls +// and resolves them individually such that they don't break the bus. +// Each section of the loop waits for the predetermined refreshrate prior to attempting to flush. // Todo: this should have a better name // If you ask "Why does this not use select over channels, share memory by communicating", // the answer is we tried, and it was cripplingly slow. @@ -18,6 +19,7 @@ func (eb *Bus) ResolvePending() { }) } +// SetRefreshRate on the event bus detailing the time to wait per attempt to ResolvePending. func (eb *Bus) SetRefreshRate(refreshRate time.Duration) { eb.refreshRate = refreshRate } diff --git a/render/particle/source.go b/render/particle/source.go index a9476d80..985dd5de 100644 --- a/render/particle/source.go +++ b/render/particle/source.go @@ -233,6 +233,13 @@ func (ps *Source) UnPause() { ps.paused = false } +// IsPaused checks for whether the source is currently in a paused state. +// It probably would have made more sense to export paused but this way if a lock is needed here in the future... +// Then it wont change the api. +func (ps *Source) IsPaused() bool { + return ps.paused +} + // ShiftX shift's a source's underlying generator func (ps *Source) ShiftX(x float64) { ps.Generator.ShiftX(x) From 73ffdb235b2d6af26e1a0d731c26f104c6c09064 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 19:40:43 -0600 Subject: [PATCH 29/40] Remove testify from timing/delay_test, add tests for DoAfterContext --- timing/delay_test.go | 56 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/timing/delay_test.go b/timing/delay_test.go index c72efe65..8a63dad4 100644 --- a/timing/delay_test.go +++ b/timing/delay_test.go @@ -1,15 +1,15 @@ package timing import ( + "context" "testing" "time" - - "github.com/stretchr/testify/assert" ) -func TestDoAfter(t *testing.T) { +func TestDoAfterCancels(t *testing.T) { + triggered := false go DoAfter(3*time.Second, func() { - t.Fatal("DoAfter did not stop") + triggered = true }) // Wait to make sure the routine started time.Sleep(1 * time.Second) @@ -22,10 +22,54 @@ outer: } } time.Sleep(3 * time.Second) - var triggered bool + if triggered { + t.Fatal("doAfter triggered") + } +} + +func TestDoAfterHappens(t *testing.T) { + triggered := false go DoAfter(1*time.Second, func() { triggered = true }) time.Sleep(2 * time.Second) - assert.True(t, triggered) + if !triggered { + t.Fatal("doAfter did not trigger") + } +} + +func TestDoAfterContextCancels(t *testing.T) { + triggered := false + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + go DoAfterContext(ctx, func() { + triggered = true + }) + // Wait to make sure the routine started + time.Sleep(1 * time.Second) +outer: + for { + select { + case ClearDelayCh <- true: + default: + break outer + } + } + time.Sleep(3 * time.Second) + if triggered { + t.Fatal("doAfterContext triggered") + } +} + +func TestDoAfterContextHappens(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + triggered := false + go DoAfterContext(ctx, func() { + triggered = true + }) + time.Sleep(2 * time.Second) + if !triggered { + t.Fatal("doAfterContext did not trigger") + } } From 7c52ad106fb479a7cf7310f3674cd1aa6a565688 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 19:49:52 -0600 Subject: [PATCH 30/40] Add tests for WeightedChooseOne --- alg/selection_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/alg/selection_test.go b/alg/selection_test.go index d00dce7a..31ec9c46 100644 --- a/alg/selection_test.go +++ b/alg/selection_test.go @@ -99,3 +99,29 @@ func TestCumulativeWeights(t *testing.T) { cum := CumulativeWeights(weights) assert.Equal(t, []float64{1, 3, 6, 10, 15, 21, 28}, cum) } + +func TestWeightedChooseOne(t *testing.T) { + weights := []float64{0, 0, 0, 0, 0, 0, 1} + remaining := RemainingWeights(weights) + choice := WeightedChooseOne(remaining) + if choice != 6 { + t.Fatalf("Expected choice of 6, got %v", choice) + } +} + +type float64er struct { + val float64 +} + +func (f *float64er) Float64() float64 { + return f.val +} + +func TestWeightedChooseOneSeeded(t *testing.T) { + weights := []float64{1, 1, 1, 1, 1, 1, 1} + remaining := RemainingWeights(weights) + choice := WeightedChooseOneSeeded(remaining, &float64er{val: 0}) + if choice != 6 { + t.Fatalf("Expected choice of 6, got %v", choice) + } +} From c2b66b94602fd5a6e6ea79d308a15bbf7d5ed78d Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 19:52:53 -0600 Subject: [PATCH 31/40] Increase float and int geom test coverage for dir constructs --- alg/floatgeom/dir_test.go | 13 +++++++++++++ alg/intgeom/dir_test.go | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 alg/floatgeom/dir_test.go create mode 100644 alg/intgeom/dir_test.go diff --git a/alg/floatgeom/dir_test.go b/alg/floatgeom/dir_test.go new file mode 100644 index 00000000..f1ee6320 --- /dev/null +++ b/alg/floatgeom/dir_test.go @@ -0,0 +1,13 @@ +package floatgeom + +import "testing" + +func TestDirMethods(t *testing.T) { + d := Dir2{10.0, 12.0} + if d.X() != 10.0 { + t.Fatalf("expected 10 for x, got %v", d.X()) + } + if d.Y() != 12.0 { + t.Fatalf("expected 12 for y, got %v", d.Y()) + } +} diff --git a/alg/intgeom/dir_test.go b/alg/intgeom/dir_test.go new file mode 100644 index 00000000..f7a852e5 --- /dev/null +++ b/alg/intgeom/dir_test.go @@ -0,0 +1,13 @@ +package intgeom + +import "testing" + +func TestDirMethods(t *testing.T) { + d := Dir2{10, 12} + if d.X() != 10 { + t.Fatalf("expected 10 for x, got %v", d.X()) + } + if d.Y() != 12 { + t.Fatalf("expected 12 for y, got %v", d.Y()) + } +} From 05ad80b150b510d196b8905fa277489790435204 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 20:08:01 -0600 Subject: [PATCH 32/40] Expand event pacakge test coverage --- event/entity_test.go | 7 +++++++ event/handler_test.go | 28 ++++++++++++++++++++++++++++ event/resolve_test.go | 23 +++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 event/resolve_test.go diff --git a/event/entity_test.go b/event/entity_test.go index 14d0107d..3ea8019b 100644 --- a/event/entity_test.go +++ b/event/entity_test.go @@ -64,3 +64,10 @@ func TestScanForEntity(t *testing.T) { }) } } + +func TestGetEntityFails(t *testing.T) { + entity := GetEntity(100) + if entity != nil { + t.Fatalf("expected nil entity, got %v", entity) + } +} diff --git a/event/handler_test.go b/event/handler_test.go index 11cea2b2..078496fd 100644 --- a/event/handler_test.go +++ b/event/handler_test.go @@ -2,6 +2,7 @@ package event import ( "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -82,3 +83,30 @@ func BenchmarkHandler(b *testing.B) { <-DefaultBus.TriggerBack(Enter, DefaultBus.framesElapsed) } } + +func TestPauseAndResume(t *testing.T) { + b := NewBus() + b.ResolvePending() + triggerCt := 0 + b.Bind(func(int, interface{}) int { + triggerCt++ + return 0 + }, "EnterFrame", 0) + ch := make(chan bool, 1000) + b.UpdateLoop(60, ch) + time.Sleep(1 * time.Second) + b.Pause() + time.Sleep(1 * time.Second) + oldCt := triggerCt + time.Sleep(1 * time.Second) + if oldCt != triggerCt { + t.Fatalf("pause did not stop enter frame from triggering: expected %v got %v", oldCt, triggerCt) + } + + b.Resume() + time.Sleep(1 * time.Second) + newCt := triggerCt + if newCt == oldCt { + t.Fatalf("resume did not resume enter frame triggering: expected %v got %v", oldCt, newCt) + } +} diff --git a/event/resolve_test.go b/event/resolve_test.go new file mode 100644 index 00000000..67d1e114 --- /dev/null +++ b/event/resolve_test.go @@ -0,0 +1,23 @@ +package event + +import ( + "testing" + "time" +) + +func TestResolvePendingWithRefreshRate(t *testing.T) { + b := NewBus() + b.SetRefreshRate(6 * time.Second) + b.ResolvePending() + failed := false + b.Bind(func(int, interface{}) int { + failed = true + return 0 + }, "EnterFrame", 0) + ch := make(chan bool, 1000) + b.UpdateLoop(60, ch) + time.Sleep(3 * time.Second) + if failed { + t.Fatal("binding was called before refresh rate should have added binding") + } +} From fa7d8ab317aa52c1e1dc1460a9599f332d73f692 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 20:23:08 -0600 Subject: [PATCH 33/40] Expand collision package test coverage --- collision/space_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/collision/space_test.go b/collision/space_test.go index f42a2d65..6237be3a 100644 --- a/collision/space_test.go +++ b/collision/space_test.go @@ -13,6 +13,12 @@ func TestSpaceFuncs(t *testing.T) { Clear() s := NewUnassignedSpace(10, 10, 10, 10) assert.NotEmpty(t, s.String()) + if s.W() != 10.0 { + t.Fatalf("expected 10 width, got %v", s.W()) + } + if s.H() != 10.0 { + t.Fatalf("expected 10 height, got %v", s.H()) + } // Getters cx, cy := s.GetCenter() @@ -62,3 +68,23 @@ func TestNewRect(t *testing.T) { assert.Equal(t, 10.0, s.GetW()) assert.Equal(t, 10.0, s.GetH()) } + +func TestNewRect2Space(t *testing.T) { + s1 := NewRect2Space(floatgeom.Rect2{ + Min: floatgeom.Point2{5, 10}, + Max: floatgeom.Point2{10, 15}, + }, 0) + s2 := NewUnassignedSpace(5, 10, 5, 5) + if s1.X() != s2.X() { + t.Fatalf("mismatched X: %v vs %v", s1.X(), s2.X()) + } + if s1.Y() != s2.Y() { + t.Fatalf("mismatched Y: %v vs %v", s1.Y(), s2.Y()) + } + if s1.W() != s2.W() { + t.Fatalf("mismatched W: %v vs %v", s1.W(), s2.W()) + } + if s1.H() != s2.H() { + t.Fatalf("mismatched H: %v vs %v", s1.H(), s2.H()) + } +} From 6e665c40fc7cad1667d2665f07052014787c58d4 Mon Sep 17 00:00:00 2001 From: implausiblyfun Date: Wed, 24 Feb 2021 21:51:08 -0500 Subject: [PATCH 34/40] fix StripOuterAlpha to actually be functional --- render/mod/filter.go | 17 +++++++++-------- render/mod/mod_test.go | 13 ++++++++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/render/mod/filter.go b/render/mod/filter.go index 24454f9b..f6d3f1b1 100644 --- a/render/mod/filter.go +++ b/render/mod/filter.go @@ -170,7 +170,7 @@ func InPlace(m Mod) Filter { } // StripOuterAlpha from the image given a source image and a alpha level to denote stripping. -// Note that this was implement for ease of implementation but not speed. +// Note that this was implemented for ease of implementation but not speed. // We could use image lib or a real depth first search to do fewer checks but this is easier... func StripOuterAlpha(m *image.RGBA, level int) Filter { l := uint8(level) @@ -193,7 +193,8 @@ func StripOuterAlpha(m *image.RGBA, level int) Filter { r := mr.RGBAAt(x, y) if r.A <= l { // Treating as transparent - rgba.Set(int(x), int(y), mr.At(x, y)) + r.A = 0 + rgba.Set(int(x), int(y), r) } else { break } @@ -205,7 +206,8 @@ func StripOuterAlpha(m *image.RGBA, level int) Filter { r := mr.RGBAAt(x, y) if r.A <= l { // Treating as transparent - rgba.Set(int(x), int(y), mr.At(x, y)) + r.A = 0 + rgba.Set(int(x), int(y), r) } else { break } @@ -218,7 +220,8 @@ func StripOuterAlpha(m *image.RGBA, level int) Filter { r := mr.RGBAAt(x, y) if r.A <= l { // Treating as transparent - rgba.Set(int(x), int(y), mr.At(x, y)) + r.A = 0 + rgba.Set(int(x), int(y), r) } else { break } @@ -230,15 +233,13 @@ func StripOuterAlpha(m *image.RGBA, level int) Filter { r := mr.RGBAAt(x, y) if r.A <= l { // Treating as transparent - rgba.Set(int(x), int(y), mr.At(x, y)) + r.A = 0 + rgba.Set(int(x), int(y), r) } else { break } } } - - // apply image onto m - } } diff --git a/render/mod/mod_test.go b/render/mod/mod_test.go index d041c7d1..3e10cad2 100644 --- a/render/mod/mod_test.go +++ b/render/mod/mod_test.go @@ -96,7 +96,18 @@ func TestAllModifications(t *testing.T) { }, { InPlace(Scale(2, 2)), setAll(newrgba(3, 3), color.RGBA{255, 0, 0, 255}), - }} + }, { + StripOuterAlpha(setOne(setOne(setOne(setOne(setAll(newrgba(3, 3), color.RGBA{255, 0, 0, 255}), + color.RGBA{0, 1, 0, 122}, 0, 0), + color.RGBA{0, 1, 0, 122}, 1, 0), + color.RGBA{0, 1, 0, 122}, 0, 1), + color.RGBA{0, 1, 0, 240}, 2, 2), 200), + setOne(setOne(setOne(setAll(newrgba(3, 3), color.RGBA{255, 0, 0, 255}), + color.RGBA{0, 1, 0, 0}, 0, 0), + color.RGBA{0, 1, 0, 0}, 1, 0), + color.RGBA{0, 1, 0, 0}, 0, 1), + }, + } for _, f := range filterList { in2 := copyrgba(in) f.Filter(in2) From b803c3153e1d40e6f623af243a5b8eea070166a6 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 20:51:10 -0600 Subject: [PATCH 35/40] Add ray caster test cases covering more utilities --- collision/ray/caster_test.go | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/collision/ray/caster_test.go b/collision/ray/caster_test.go index 9b92af9a..c35962e5 100644 --- a/collision/ray/caster_test.go +++ b/collision/ray/caster_test.go @@ -54,6 +54,142 @@ func TestCasterScene(t *testing.T) { expected: []*collision.Space{ spaces["102-202"], }, + }, { + name: "Accept Filters", + setup: func() { + tree1.Add(spaces["100-200"]) + tree1.Add(spaces["101-201"]) + tree1.Add(spaces["102-202"]) + }, + teardown: func() { + tree1.Delete(spaces["100-200"]) + tree1.Delete(spaces["101-201"]) + tree1.Delete(spaces["102-202"]) + }, + opts: []CastOption{ + Tree(tree1), + CenterPoints(true), + Distance(50), + PointSize(floatgeom.Point2{.001, .001}), + PointSpan(2), + LimitResults(2), + AcceptLabels(100), + AcceptIDs(200), + }, + defCaster: NewCaster(), + origin: floatgeom.Point2{5, 5}, + target: floatgeom.Point2{25, 25}, + expected: []*collision.Space{ + spaces["100-200"], + }, + }, { + name: "StopAtLabel", + setup: func() { + tree1.Add(spaces["100-200"]) + tree1.Add(spaces["101-201"]) + tree1.Add(spaces["102-202"]) + }, + teardown: func() { + tree1.Delete(spaces["100-200"]) + tree1.Delete(spaces["101-201"]) + tree1.Delete(spaces["102-202"]) + }, + opts: []CastOption{ + Tree(tree1), + CenterPoints(true), + Distance(50), + PointSize(floatgeom.Point2{.001, .001}), + PointSpan(2), + StopAtLabel(100), + }, + defCaster: NewCaster(), + origin: floatgeom.Point2{5, 5}, + target: floatgeom.Point2{25, 25}, + expected: []*collision.Space{ + spaces["100-200"], + }, + }, { + name: "StopAtID", + setup: func() { + tree1.Add(spaces["100-200"]) + tree1.Add(spaces["101-201"]) + tree1.Add(spaces["102-202"]) + }, + teardown: func() { + tree1.Delete(spaces["100-200"]) + tree1.Delete(spaces["101-201"]) + tree1.Delete(spaces["102-202"]) + }, + opts: []CastOption{ + Tree(tree1), + CenterPoints(true), + Distance(50), + PointSize(floatgeom.Point2{.001, .001}), + PointSpan(2), + StopAtID(201), + }, + defCaster: NewCaster(), + origin: floatgeom.Point2{5, 5}, + target: floatgeom.Point2{25, 25}, + expected: []*collision.Space{ + spaces["100-200"], + spaces["101-201"], + }, + }, { + name: "StopAtNothing", + setup: func() { + tree1.Add(spaces["100-200"]) + tree1.Add(spaces["101-201"]) + tree1.Add(spaces["102-202"]) + }, + teardown: func() { + tree1.Delete(spaces["100-200"]) + tree1.Delete(spaces["101-201"]) + tree1.Delete(spaces["102-202"]) + }, + opts: []CastOption{ + Tree(tree1), + CenterPoints(true), + Distance(50), + PointSize(floatgeom.Point2{.001, .001}), + PointSpan(2), + StopAtID(203), + StopAtLabel(103), + }, + defCaster: NewCaster(), + origin: floatgeom.Point2{5, 5}, + target: floatgeom.Point2{25, 25}, + expected: []*collision.Space{ + spaces["100-200"], + spaces["101-201"], + spaces["102-202"], + }, + }, { + name: "Pierce", + setup: func() { + tree1.Add(spaces["100-200"]) + tree1.Add(spaces["101-201"]) + tree1.Add(spaces["102-202"]) + }, + teardown: func() { + tree1.Delete(spaces["100-200"]) + tree1.Delete(spaces["101-201"]) + tree1.Delete(spaces["102-202"]) + }, + opts: []CastOption{ + Tree(tree1), + CenterPoints(true), + Distance(50), + PointSize(floatgeom.Point2{.001, .001}), + PointSpan(2), + Pierce(2), + }, + defCaster: NewCaster(), + origin: floatgeom.Point2{5, 5}, + target: floatgeom.Point2{25, 25}, + expected: []*collision.Space{ + spaces["102-202"], + }, }, } for _, tc := range tcs { @@ -75,3 +211,12 @@ func TestCasterScene(t *testing.T) { }) } } + +func TestNewCasterDefaultTree(t *testing.T) { + DefaultCaster.Tree = nil + c := NewCaster() + DefaultCaster.Tree = collision.DefTree + if c.Tree == nil { + t.Fatal("nil caster tree should have been set to default tree") + } +} From da2f90a1e0c0bac4023da695ef31b34f83c25f67 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 21:16:34 -0600 Subject: [PATCH 36/40] expand render utilities test coverage --- render/decoder_test.go | 27 +++++++++++++++++++++++---- render/noopStackable_test.go | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 render/noopStackable_test.go diff --git a/render/decoder_test.go b/render/decoder_test.go index 3d08cec0..b88c4a75 100644 --- a/render/decoder_test.go +++ b/render/decoder_test.go @@ -2,11 +2,30 @@ package render import ( "testing" - - "github.com/stretchr/testify/assert" ) +// We are rather intentionally not testing that the decoders work, +// partially because it's a lot of work and partially because decoding images is not +// this package's job (it just calls decoders). It'd be a lot of pointless mocking. + func TestRegisterDecoder(t *testing.T) { - assert.NotNil(t, RegisterDecoder(".png", nil)) - assert.Nil(t, RegisterDecoder(".new", nil)) + err := RegisterDecoder(".png", nil) + if err == nil { + t.Fatal("expected registering .png to fail") + } + err = RegisterDecoder(".new", nil) + if err != nil { + t.Fatalf("expected registering .new to suceed: %v", err) + } +} + +func TestRegisterCfgDecoder(t *testing.T) { + err := RegisterCfgDecoder(".png", nil) + if err == nil { + t.Fatal("expected registering .png to fail") + } + err = RegisterCfgDecoder(".new", nil) + if err != nil { + t.Fatalf("expected registering .new to suceed: %v", err) + } } diff --git a/render/noopStackable_test.go b/render/noopStackable_test.go new file mode 100644 index 00000000..53749567 --- /dev/null +++ b/render/noopStackable_test.go @@ -0,0 +1,22 @@ +package render + +import ( + "image" + "testing" +) + +func TestNoopStackable(t *testing.T) { + noop := NoopStackable{} + // these calls are noops + noop.PreDraw() + noop.Replace(nil, nil, -142) + noop.draw(nil, image.Point{}, -124, 23) + r := noop.Add(nil, 01, 124, 04, 2) + if r != nil { + t.Fatalf("expected nil renderable from Add, got %v", r) + } + noop2 := noop.Copy() + if noop2 != noop { + t.Fatalf("expected equal noop stackables") + } +} From eb8dab94bed559f39988f2bd720fa67a2c5d0ab8 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 22:03:15 -0600 Subject: [PATCH 37/40] Add dlog test coverage and fix variadic expansion bug in default logger log calls --- dlog/default.go | 8 ++-- dlog/default_test.go | 53 ++++++++++++++++++++++++++ dlog/dlog_private_test.go | 21 +++++++++++ dlog/dlog_test.go | 78 +++++++++++++++++++++++++++++++++++++++ dlog/fullLogger.go | 2 +- dlog/levels_test.go | 44 ++++++++++++++++++++++ 6 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 dlog/default_test.go create mode 100644 dlog/dlog_private_test.go create mode 100644 dlog/dlog_test.go create mode 100644 dlog/levels_test.go diff --git a/dlog/default.go b/dlog/default.go index fd6fa58a..179bdd90 100644 --- a/dlog/default.go +++ b/dlog/default.go @@ -144,27 +144,27 @@ func (l *logger) CreateLogFile() { // Error will write a dlog if the debug level is not NONE func (l *logger) Error(in ...interface{}) { if l.debugLevel > NONE { - l.dLog(true, true, in) + l.dLog(true, true, in...) } } // Warn will write a dLog if the debug level is higher than ERROR func (l *logger) Warn(in ...interface{}) { if l.debugLevel > ERROR { - l.dLog(true, true, in) + l.dLog(true, true, in...) } } // Info will write a dLog if the debug level is higher than WARN func (l *logger) Info(in ...interface{}) { if l.debugLevel > WARN { - l.dLog(true, false, in) + l.dLog(true, false, in...) } } // Verb will write a dLog if the debug level is higher than INFO func (l *logger) Verb(in ...interface{}) { if l.debugLevel > INFO { - l.dLog(true, false, in) + l.dLog(true, false, in...) } } diff --git a/dlog/default_test.go b/dlog/default_test.go new file mode 100644 index 00000000..c70139a2 --- /dev/null +++ b/dlog/default_test.go @@ -0,0 +1,53 @@ +package dlog + +import ( + "bufio" + "bytes" + "testing" +) + +func TestLogger(t *testing.T) { + lgr := NewLogger().(*logger) + + defaultLevel := lgr.GetLogLevel() + if defaultLevel != ERROR { + t.Fatalf("expected default log level to be ERROR, was: %v", defaultLevel) + } + + lgr.SetDebugLevel(-1) + if lgr.GetLogLevel() != NONE { + t.Fatalf("expected -1 log level to be NONE, was: %v", lgr.GetLogLevel()) + } + + lgr.SetDebugLevel(VERBOSE) + + var buff = new(bytes.Buffer) + + lgr.writer = bufio.NewWriter(buff) + + lgr.FileWrite("fileWrite") + lgr.Error("error") + lgr.Warn("warn") + lgr.Info("info") + lgr.Verb("verb") + + lgr.SetDebugFilter("foo") + lgr.Verb("bar") + lgr.Verb("foo") + + expectedOut := `[testing:1194] VERBOSE:fileWrite +[testing:1194] VERBOSE:error +[testing:1194] VERBOSE:warn +[testing:1194] VERBOSE:info +[testing:1194] VERBOSE:verb +[testing:1194] VERBOSE:foo +` + + out := string(buff.Bytes()) + + if out != expectedOut { + t.Fatalf("logged output did not match: got %q expected %q", out, expectedOut) + } + + lgr.CreateLogFile() +} diff --git a/dlog/dlog_private_test.go b/dlog/dlog_private_test.go new file mode 100644 index 00000000..ef3018d4 --- /dev/null +++ b/dlog/dlog_private_test.go @@ -0,0 +1,21 @@ +package dlog + +import "testing" + +func TestSetCustomLogger(t *testing.T) { + type customLogger struct { + FullLogger + } + cl := customLogger{} + SetLogger(cl) + + SetLogger(&logger{}) + + _, isCustom := oakLogger.(customLogger) + if !isCustom { + t.Fatal("custom logger should not have been overwritten by default logger") + } + + oakLogger = nil + fullOakLogger = nil +} diff --git a/dlog/dlog_test.go b/dlog/dlog_test.go new file mode 100644 index 00000000..bfd5343f --- /dev/null +++ b/dlog/dlog_test.go @@ -0,0 +1,78 @@ +package dlog_test + +import ( + "fmt" + "testing" + + "github.com/oakmound/oak/v2/dlog" +) + +func TestErrorCheck(t *testing.T) { + called := false + dlog.Error = func(...interface{}) { + called = true + } + dlog.ErrorCheck(nil) + if called { + t.Fatal("error should not have been called on nil error") + } + dlog.ErrorCheck(fmt.Errorf("err")) + if !called { + t.Fatal("error should have been called on real error") + } + dlog.Error = func(...interface{}) {} +} + +func TestParseDebugLevel(t *testing.T) { + type testCase struct { + in string + outLevel dlog.Level + outErrors bool + outErrorStr string + } + tcs := []testCase{ + { + in: "info", + outLevel: dlog.INFO, + }, { + in: "InFo", + outLevel: dlog.INFO, + }, { + in: "verbose", + outLevel: dlog.VERBOSE, + }, { + in: "ERROR", + outLevel: dlog.ERROR, + }, { + in: "warN", + outLevel: dlog.WARN, + }, { + in: "none", + outLevel: dlog.NONE, + }, { + in: "other", + outErrors: true, + outErrorStr: "parsing dlog level of \"OTHER\" failed", + }, + } + for _, tc := range tcs { + tc := tc + t.Run(tc.in, func(t *testing.T) { + lvl, err := dlog.ParseDebugLevel(tc.in) + if tc.outErrors { + if err == nil { + t.Fatal("expected error") + } + if tc.outErrorStr != "" { + if tc.outErrorStr != err.Error() { + t.Fatalf("error did not match: got %v expected %v", err.Error(), tc.outErrorStr) + } + } + return + } + if lvl != tc.outLevel { + t.Fatalf("level did not match: got %v expected %v", lvl, tc.outLevel) + } + }) + } +} diff --git a/dlog/fullLogger.go b/dlog/fullLogger.go index 959fcc10..3949ffda 100644 --- a/dlog/fullLogger.go +++ b/dlog/fullLogger.go @@ -15,7 +15,7 @@ type FullLogger interface { var fullOakLogger FullLogger // GetLogLevel returns the log level of the fullOakLogger, or -// NONE if there is not fullOakLogger. +// NONE if there is no fullOakLogger. var GetLogLevel = func() Level { return NONE } diff --git a/dlog/levels_test.go b/dlog/levels_test.go new file mode 100644 index 00000000..40238216 --- /dev/null +++ b/dlog/levels_test.go @@ -0,0 +1,44 @@ +package dlog_test + +import ( + "testing" + "github.com/oakmound/oak/v2/dlog" +) + + +func TestLevelsString(t *testing.T) { + type testCase struct { + in dlog.Level + out string + } + tcs := []testCase{ + { + in: dlog.NONE, + out: "NONE", + }, { + in: dlog.ERROR, + out: "ERROR", + }, { + in: dlog.WARN, + out: "WARN", + }, { + in: dlog.INFO, + out: "INFO", + }, { + in: dlog.VERBOSE, + out: "VERBOSE", + }, { + in: dlog.Level(100), + out: "", + }, + } + for _, tc := range tcs { + tc := tc + t.Run(tc.out, func(t *testing.T) { + out := tc.in.String() + if out != tc.out { + t.Fatalf("mismatched output, got %v expected %v", out, tc.out) + } + }) + } +} From e1e79d513f3f7c0b0aff56803baff1c079079fcb Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 22:05:49 -0600 Subject: [PATCH 38/40] Add dlog to test coverage script --- cover.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cover.sh b/cover.sh index 5bed4f43..d1a419d1 100755 --- a/cover.sh +++ b/cover.sh @@ -85,6 +85,11 @@ if [ -f profile.out ]; then rm profile.out fi go test -coverprofile=profile.out -covermode=atomic ./scene +if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out +fi +go test -coverprofile=profile.out -covermode=atomic ./dlog if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out From 0dae2bc9c69ff6e161036d271680d2efe07a7ba3 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 22:13:37 -0600 Subject: [PATCH 39/40] Update log line in dlog tests to a reproducible line in the test itself --- dlog/default_test.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/dlog/default_test.go b/dlog/default_test.go index c70139a2..231f9e16 100644 --- a/dlog/default_test.go +++ b/dlog/default_test.go @@ -25,22 +25,25 @@ func TestLogger(t *testing.T) { lgr.writer = bufio.NewWriter(buff) - lgr.FileWrite("fileWrite") - lgr.Error("error") - lgr.Warn("warn") - lgr.Info("info") - lgr.Verb("verb") - - lgr.SetDebugFilter("foo") - lgr.Verb("bar") - lgr.Verb("foo") - - expectedOut := `[testing:1194] VERBOSE:fileWrite -[testing:1194] VERBOSE:error -[testing:1194] VERBOSE:warn -[testing:1194] VERBOSE:info -[testing:1194] VERBOSE:verb -[testing:1194] VERBOSE:foo + callLogger := func() { + lgr.FileWrite("fileWrite") + lgr.Error("error") + lgr.Warn("warn") + lgr.Info("info") + lgr.Verb("verb") + + lgr.SetDebugFilter("foo") + lgr.Verb("bar") + lgr.Verb("foo") + } + callLogger() + + expectedOut := `[default_test:39] VERBOSE:fileWrite +[default_test:39] VERBOSE:error +[default_test:39] VERBOSE:warn +[default_test:39] VERBOSE:info +[default_test:39] VERBOSE:verb +[default_test:39] VERBOSE:foo ` out := string(buff.Bytes()) From 1fdc97e46b8d0fafe8ca4b1a31adc0fb5bfe8035 Mon Sep 17 00:00:00 2001 From: Patrick Stephen Date: Wed, 24 Feb 2021 22:39:12 -0600 Subject: [PATCH 40/40] Upgrade github actions to 1.16 --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e48e182a..3d080700 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.13 + - name: Set up Go 1.16 uses: actions/setup-go@v1 with: - go-version: 1.13 + go-version: 1.16 id: go - name: Check out code into the Go module directory