diff --git a/cover.sh b/cover.sh index d1a419d1..867d67d7 100755 --- a/cover.sh +++ b/cover.sh @@ -9,82 +9,82 @@ echo "" > coverage.txt # # Don't run coverage on examples, just test that they compile go test ./examples/... -go test -coverprofile=profile.out -covermode=atomic ./shape +go test -coverprofile=profile.out -covermode=atomic ./alg if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./oakerr +go test -coverprofile=profile.out -covermode=atomic ./alg/intgeom if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./timing +go test -coverprofile=profile.out -covermode=atomic ./alg/floatgeom if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./physics +go test -coverprofile=profile.out -covermode=atomic ./collision if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./event +go test -coverprofile=profile.out -covermode=atomic ./collision/ray if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./render +go test -coverprofile=profile.out -covermode=atomic ./dlog if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./render/mod +go test -coverprofile=profile.out -covermode=atomic ./event if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./render/particle +go test -coverprofile=profile.out -covermode=atomic ./fileutil if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./alg +go test -coverprofile=profile.out -covermode=atomic ./mouse if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./alg/intgeom +go test -coverprofile=profile.out -covermode=atomic ./oakerr if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./alg/floatgeom +go test -coverprofile=profile.out -covermode=atomic ./physics if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./collision +go test -coverprofile=profile.out -covermode=atomic ./render if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./collision/ray +go test -coverprofile=profile.out -covermode=atomic ./render/mod if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./fileutil +go test -coverprofile=profile.out -covermode=atomic ./render/particle if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out fi -go test -coverprofile=profile.out -covermode=atomic ./mouse +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 ./scene +go test -coverprofile=profile.out -covermode=atomic ./shape if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out @@ -93,4 +93,9 @@ go test -coverprofile=profile.out -covermode=atomic ./dlog if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out -fi \ No newline at end of file +fi +go test -coverprofile=profile.out -covermode=atomic ./timing +if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out +fi diff --git a/dlog/default.go b/dlog/default.go index 179bdd90..d8f07e3c 100644 --- a/dlog/default.go +++ b/dlog/default.go @@ -12,6 +12,10 @@ import ( "time" ) +var ( + _ FullLogger = &logger{} +) + type logger struct { bytPool sync.Pool debugLevel Level @@ -98,11 +102,12 @@ func truncateFileName(f string) string { } func (l *logger) checkFilter(f string, in ...interface{}) bool { - ret := false for _, elem := range in { - ret = ret || strings.Contains(fmt.Sprintf("%s", elem), l.debugFilter) + if strings.Contains(fmt.Sprintf("%s", elem), l.debugFilter) { + return true + } } - return ret || strings.Contains(f, l.debugFilter) + return strings.Contains(f, l.debugFilter) } // SetDebugFilter sets the string which determines diff --git a/dlog/dlog.go b/dlog/dlog.go index ceee7db5..8df02938 100644 --- a/dlog/dlog.go +++ b/dlog/dlog.go @@ -21,10 +21,12 @@ var oakLogger Logger // ErrorCheck checks that the input is not nil, then calls Error on it if it is // not. Otherwise it does nothing. -func ErrorCheck(in error) { +// Emits the input error as is for additional processing if desired. +func ErrorCheck(in error) error { if in != nil { Error(in) } + return in } // Error will write a log if the debug level is not NONE diff --git a/dlog/regexLogger.go b/dlog/regexLogger.go new file mode 100644 index 00000000..e14b2398 --- /dev/null +++ b/dlog/regexLogger.go @@ -0,0 +1,182 @@ +package dlog + +import ( + "bytes" + "fmt" + "io" + "os" + "regexp" + "runtime" + "strconv" + "strings" + "time" +) + +var ( + _ FullLogger = &RegexLogger{} +) + +// RegexLogger is a logger implementation that offers some +// additional features on top of the default logger. +// Todo v3: combine logger implementations. +type RegexLogger struct { + debugLevel Level + + debugFilter string + filterRegex *regexp.Regexp + // FilterOverrideLevel is the log level at which + // logs will be shown regardless of the filter. + FilterOverrideLevel Level + + writer io.Writer + file io.Writer +} + +// NewRegexLogger returns a custom logger that writes to os.Stdout and +// overrides filters on WARN or higher messages. +func NewRegexLogger(level Level) *RegexLogger { + return &RegexLogger{ + debugLevel: level, + writer: os.Stdout, + FilterOverrideLevel: WARN, + } +} + +// GetLogLevel returns the current log level, i.e WARN or INFO... +func (l *RegexLogger) GetLogLevel() Level { + return l.debugLevel +} + +// dLog, the primary function of the package, +// prints out and writes to file a string +// containing the logged data separated by spaces, +// prepended with file and line information. +// It only includes logs which pass the current filters. +func (l *RegexLogger) dLog(w io.Writer, override bool, in ...interface{}) { + //(pc uintptr, file string, line int, ok bool) + _, f, line, ok := runtime.Caller(2) + if strings.Contains(f, "dlog") { + _, f, line, ok = runtime.Caller(3) + } + if ok { + var bldr strings.Builder + f = truncateFileName(f) + // Note on errors: these functions all return + // errors, but they are always nil. + bldr.WriteRune('[') + bldr.WriteString(f) + bldr.WriteRune(':') + bldr.WriteString(strconv.Itoa(line)) + bldr.WriteString("] ") + bldr.WriteString(logLevels[l.GetLogLevel()]) + bldr.WriteRune(':') + for _, elem := range in { + bldr.WriteString(fmt.Sprintf("%v ", elem)) + } + bldr.WriteRune('\n') + fullLog := []byte(bldr.String()) + + if !override && !l.checkFilter(fullLog) { + return + } + + _, err := w.Write(fullLog) + if err != nil { + fmt.Println("Logging error", err) + } + } +} + +func (l *RegexLogger) checkFilter(fullLog []byte) bool { + if l.debugFilter == "" { + return true + } + if l.filterRegex != nil { + return l.filterRegex.Match(fullLog) + } + return bytes.Contains(fullLog, []byte(l.debugFilter)) +} + +// SetDebugFilter sets the string which determines +// what debug messages get printed. Only messages +// which contain the filer as a pseudo-regex +func (l *RegexLogger) SetDebugFilter(filter string) { + l.debugFilter = filter + var err error + l.filterRegex, err = regexp.Compile(filter) + if err != nil { + l.Error("could not compile filter regex", err) + } +} + +// SetDebugLevel sets what message levels of debug +// will be printed. +func (l *RegexLogger) SetDebugLevel(dL Level) { + if dL < NONE || dL > VERBOSE { + l.Warn("Unknown debug level: ", dL) + l.debugLevel = NONE + } else { + l.debugLevel = dL + } +} + +// CreateLogFile creates a file in the 'logs' directory +// of the starting point of this program to write logs to +func (l *RegexLogger) CreateLogFile() { + file := "logs/dlog" + file += time.Now().Format("_Jan_2_15-04-05_2006") + file += ".txt" + var err error + l.file, err = os.Create(file) + if err != nil { + fmt.Println("[oak]-------- No logs directory found. No logs will be written to file.") + return + } + l.writer = io.MultiWriter(l.file, l.writer) +} + +// FileWrite acts just like a regular write on a RegexLogger. It does +// not respect overrides. +func (l *RegexLogger) FileWrite(in ...interface{}) { + if l.file == nil { + return + } + l.dLog(l.file, true, in...) +} + +// Error will write a dlog if the debug level is not NONE +func (l *RegexLogger) Error(in ...interface{}) { + if l.debugLevel > NONE { + l.dLog(l.writer, l.FilterOverrideLevel > NONE, in) + } +} + +// Warn will write a dLog if the debug level is higher than ERROR +func (l *RegexLogger) Warn(in ...interface{}) { + if l.debugLevel > ERROR { + l.dLog(l.writer, l.FilterOverrideLevel > ERROR, in) + } +} + +// Info will write a dLog if the debug level is higher than WARN +func (l *RegexLogger) Info(in ...interface{}) { + if l.debugLevel > WARN { + l.dLog(l.writer, l.FilterOverrideLevel > WARN, in) + } +} + +// Verb will write a dLog if the debug level is higher than INFO +func (l *RegexLogger) Verb(in ...interface{}) { + if l.debugLevel > INFO { + l.dLog(l.writer, l.FilterOverrideLevel > INFO, in) + } +} + +// SetWriter sets the writer that RegexLogger logs to +func (l *RegexLogger) SetWriter(w io.Writer) error { + if w == nil { + return fmt.Errorf("cannot write to nil writer") + } + l.writer = w + return nil +} diff --git a/dlog/regexLogger_test.go b/dlog/regexLogger_test.go new file mode 100644 index 00000000..7060b9eb --- /dev/null +++ b/dlog/regexLogger_test.go @@ -0,0 +1,222 @@ +package dlog + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewRegexLogger(t *testing.T) { + cl := NewRegexLogger(NONE) + require.NotNil(t, cl) + require.Equal(t, cl.debugLevel, NONE) + require.Equal(t, cl.FilterOverrideLevel, WARN) +} + +func TestRegexLogger_GetLogLevel(t *testing.T) { + cl := NewRegexLogger(NONE) + level := cl.GetLogLevel() + require.Equal(t, level, NONE) +} + +func testRegexLoggerContains(t *testing.T, f func(cl *RegexLogger, buff *bytes.Buffer), s string) { + cl := NewRegexLogger(VERBOSE) + buff := bytes.NewBuffer([]byte{}) + cl.SetWriter(buff) + f(cl, buff) + if len(s) == 0 { + require.Equal(t, "", buff.String()) + } else { + require.Contains(t, buff.String(), s) + } +} + +func TestRegexLogger_SetDebugFilter(t *testing.T) { + type testCase struct { + name string + fn func(cl *RegexLogger, buff *bytes.Buffer) + contains string + } + tcs := []testCase{ + { + name: "valid regex: no match", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("myfilter(1)+") + cl.Verb("20145") + }, + }, { + name: "valid regex: no match 2", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("myfilter(1)+") + cl.Verb("myfilter") + }, + }, { + name: "valid regex: match", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("myfilter(1)+") + cl.Verb("myfilter11111") + }, + contains: "myfilter11111", + }, { + name: "invalid regex", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("([1-9)+") + }, + contains: "could not compile filter regex", + }, { + name: "invalid regex: no match", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("([1-9)+") + buff.Reset() + cl.Verb("1423") + }, + }, { + name: "invalid regex: match", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("([1-9)+") + buff.Reset() + cl.Verb("([1-9)+") + }, + contains: "([1-9)+", + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + testRegexLoggerContains(t, tc.fn, tc.contains) + }) + } +} + +func TestRegexLogger_FilterOverrideLevel(t *testing.T) { + type testCase struct { + name string + fn func(cl *RegexLogger, buff *bytes.Buffer) + contains string + } + tcs := []testCase{ + { + name: "default override: too low does not emit", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("filter1") + cl.Verb("does not contain") + }, + }, { + name: "default override: emits", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("filter1") + cl.Warn("does not contain") + }, + contains: "does not contain", + }, { + name: "custom override: too low does not emit", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("filter1") + cl.FilterOverrideLevel = ERROR + cl.Warn("does not contain") + }, + }, { + name: "custom override: emits", + fn: func(cl *RegexLogger, buff *bytes.Buffer) { + cl.SetDebugFilter("filter1") + cl.FilterOverrideLevel = ERROR + cl.Error("does not contain") + }, + contains: "does not contain", + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + testRegexLoggerContains(t, tc.fn, tc.contains) + }) + } +} + +func TestRegexLogger_SetDebugLevel(t *testing.T) { + cl := NewRegexLogger(NONE) + cl.SetDebugLevel(INFO) + require.Equal(t, cl.debugLevel, INFO) + + level := cl.GetLogLevel() + require.Equal(t, level, INFO) +} + +func TestRegexLogger_CreateLogFile(t *testing.T) { + t.Skip("not worth mocking os") +} + +func TestRegexLogger_FileWrite(t *testing.T) { + cl := NewRegexLogger(NONE) + buff := bytes.NewBuffer([]byte{}) + cl.file = buff + cl.FileWrite("test") + require.Contains(t, buff.String(), "test") +} + +func TestRegexLogger_Error(t *testing.T) { + cl := NewRegexLogger(ERROR) + buff := bytes.NewBuffer([]byte{}) + cl.SetWriter(buff) + cl.Verb("verbose") + cl.Info("info") + cl.Warn("warn") + cl.Error("error") + logged := buff.String() + require.NotContains(t, logged, "verbose") + require.NotContains(t, logged, "info") + require.NotContains(t, logged, "warn") + require.Contains(t, logged, "error") +} + +func TestRegexLogger_Warn(t *testing.T) { + cl := NewRegexLogger(WARN) + buff := bytes.NewBuffer([]byte{}) + cl.SetWriter(buff) + cl.Verb("verbose") + cl.Info("info") + cl.Warn("warn") + cl.Error("error") + logged := buff.String() + require.NotContains(t, logged, "verbose") + require.NotContains(t, logged, "info") + require.Contains(t, logged, "warn") + require.Contains(t, logged, "error") +} + +func TestRegexLogger_Info(t *testing.T) { + cl := NewRegexLogger(INFO) + buff := bytes.NewBuffer([]byte{}) + cl.SetWriter(buff) + cl.Verb("verbose") + cl.Info("info") + cl.Warn("warn") + cl.Error("error") + logged := buff.String() + require.NotContains(t, logged, "verbose") + require.Contains(t, logged, "info") + require.Contains(t, logged, "warn") + require.Contains(t, logged, "error") +} + +func TestRegexLogger_Verb(t *testing.T) { + cl := NewRegexLogger(VERBOSE) + buff := bytes.NewBuffer([]byte{}) + cl.SetWriter(buff) + cl.Verb("verbose") + cl.Info("info") + cl.Warn("warn") + cl.Error("error") + logged := buff.String() + require.Contains(t, logged, "verbose") + require.Contains(t, logged, "info") + require.Contains(t, logged, "warn") + require.Contains(t, logged, "error") +} + +func TestRegexLogger_SetWriter(t *testing.T) { + cl := NewRegexLogger(VERBOSE) + err := cl.SetWriter(nil) + require.NotNil(t, err) + err = cl.SetWriter(bytes.NewBuffer([]byte{})) + require.Nil(t, err) +} diff --git a/drawLoop.go b/drawLoop.go index 63ca2e44..1cf3fb2f 100644 --- a/drawLoop.go +++ b/drawLoop.go @@ -62,7 +62,7 @@ func drawLoop() { dlog.Verb("Got something from viewport channel (waiting on draw)") updateScreen(viewPoint[0], viewPoint[1]) case viewPoint := <-viewportShiftCh: - dlog.Verb("Got something from viewport shfit channel (waiting on draw)") + dlog.Verb("Got something from viewport shift channel (waiting on draw)") shiftViewPort(viewPoint[0], viewPoint[1]) default: } diff --git a/examples/keyboard-test/main.go b/examples/keyboard-test/main.go new file mode 100644 index 00000000..fe4eda42 --- /dev/null +++ b/examples/keyboard-test/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "sync" + + oak "github.com/oakmound/oak/v2" + "github.com/oakmound/oak/v2/event" + "github.com/oakmound/oak/v2/key" + "github.com/oakmound/oak/v2/render" + "github.com/oakmound/oak/v2/scene" +) + +var keyLock sync.Mutex +var keys = map[string]struct{}{} + +type stringStringer string + +func (ss stringStringer) String() string { + return string(ss) +} + +func main() { + oak.Add("keyboard-test", func(string, interface{}) { + kRenderable := render.NewStrText("", 40, 40) + render.Draw(kRenderable, 0) + event.GlobalBind(func(_ int, k interface{}) int { + kValue := k.(string) + keyLock.Lock() + keys[kValue] = struct{}{} + txt := "" + for k := range keys { + txt += k + "\n" + } + kRenderable.SetText(stringStringer(txt)) + keyLock.Unlock() + return 0 + }, key.Down) + event.GlobalBind(func(_ int, k interface{}) int { + kValue := k.(string) + keyLock.Lock() + delete(keys, kValue) + txt := "" + for k := range keys { + txt += k + " " + } + kRenderable.SetText(stringStringer(txt)) + keyLock.Unlock() + return 0 + }, key.Up) + }, func() bool { + return true + }, func() (string, *scene.Result) { + return "keyboard-test", nil + }) + oak.Init("keyboard-test") +} \ No newline at end of file diff --git a/go.mod b/go.mod index 059d8e0e..47625a15 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 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/shiny v0.4.2 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 diff --git a/inputLoop.go b/inputLoop.go index 462de5b2..f6eb26b2 100644 --- a/inputLoop.go +++ b/inputLoop.go @@ -58,16 +58,11 @@ func inputLoop() { k := GetKeyBind(e.Code.String()[4:]) switch e.Direction { case key.DirPress: - setDown(k) - logicHandler.Trigger(okey.Down, k) - logicHandler.Trigger(okey.Down+k, nil) + TriggerKeyDown(k) case key.DirRelease: - setUp(k) - logicHandler.Trigger(okey.Up, k) - logicHandler.Trigger(okey.Up+k, nil) + TriggerKeyUp(k) default: - logicHandler.Trigger(okey.Held, k) - logicHandler.Trigger(okey.Held+k, nil) + TriggerKeyHeld(k) } // Send mouse events @@ -86,11 +81,6 @@ func inputLoop() { case mouse.Event: button := omouse.GetMouseButton(e.Button) eventName := omouse.GetEventName(e.Direction, e.Button) - if e.Direction == mouse.DirPress { - setDown(button) - } else if e.Direction == mouse.DirRelease { - setUp(button) - } // The event triggered for mouse events has the same scaling as the // render and collision space. I.e. if the viewport is at 0, the mouse's // position is exactly the same as the position of a visible entity @@ -104,9 +94,7 @@ func inputLoop() { button, eventName, ) - - omouse.Propagate(eventName+"On", mevent) - logicHandler.Trigger(eventName, mevent) + TriggerMouseEvent(mevent) case gesture.Event: eventName := "Gesture" + e.Type.String() @@ -122,3 +110,48 @@ func inputLoop() { } } } + +// TriggerKeyDown triggers a software-emulated keypress. +// This should be used cautiously when the keyboard is in use. +// From the perspective of the event handler this is indistinguishable +// from a real keypress. +func TriggerKeyDown(k string) { + SetDown(k) + logicHandler.Trigger(okey.Down, k) + logicHandler.Trigger(okey.Down+k, nil) +} + +// TriggerKeyUp triggers a software-emulated key release. +// This should be used cautiously when the keyboard is in use. +// From the perspective of the event handler this is indistinguishable +// from a real key release. +func TriggerKeyUp(k string) { + SetUp(k) + logicHandler.Trigger(okey.Up, k) + logicHandler.Trigger(okey.Up+k, nil) +} + +// TriggerKeyHeld triggers a software-emulated key hold signal. +// This should be used cautiously when the keyboard is in use. +// From the perspective of the event handler this is indistinguishable +// from a real key hold signal. +func TriggerKeyHeld(k string) { + logicHandler.Trigger(okey.Held, k) + logicHandler.Trigger(okey.Held+k, nil) +} + +// TriggerMouseEvent triggers a software-emulated mouse event. +// This should be used cautiously when the mouse is in use. +// From the perspective of the event handler this is indistinguishable +// from a real key mouse press or movement. +func TriggerMouseEvent(mevent omouse.Event) { + switch mevent.Event { + case omouse.Press: + SetDown(mevent.Button) + case omouse.Release: + SetUp(mevent.Button) + } + + omouse.Propagate(mevent.Event+"On", mevent) + logicHandler.Trigger(mevent.Event, mevent) +} diff --git a/inputTracker.go b/inputTracker.go index 17ce08b8..9f1f675b 100644 --- a/inputTracker.go +++ b/inputTracker.go @@ -12,6 +12,7 @@ import ( ) // InputType expresses some form of input to the engine to represent a player +// Todo v3: convert into int32 for use with atomic.SwapInt32 type InputType int // Supported Input Types diff --git a/keyStore.go b/keyStore.go index 9287e1d1..1d33518a 100644 --- a/keyStore.go +++ b/keyStore.go @@ -16,7 +16,13 @@ var ( // control access to a keystate map // from key strings to down or up boolean // states. -func setUp(key string) { + +// SetUp will cause later IsDown calls to report false +// for the given key. This is called internally when +// events are sent from the real keyboard and mouse. +// Calling this can interrupt real input or cause +// unintended behavior and should be done cautiously. +func SetUp(key string) { keyLock.Lock() durationLock.Lock() delete(keyState, key) @@ -25,7 +31,12 @@ func setUp(key string) { keyLock.Unlock() } -func setDown(key string) { +// SetDown will cause later IsDown calls to report true +// for the given key. This is called internally when +// events are sent from the real keyboard and mouse. +// Calling this can interrupt real input or cause +// unintended behavior and should be done cautiously. +func SetDown(key string) { keyLock.Lock() keyState[key] = true keyDurations[key] = time.Now() @@ -41,6 +52,7 @@ func IsDown(key string) (k bool) { } // IsHeld returns whether a key is held down, and for how long +// it has been held. func IsHeld(key string) (k bool, d time.Duration) { keyLock.RLock() k = keyState[key] diff --git a/keyStore_test.go b/keyStore_test.go index 3281bc19..5c95ab24 100644 --- a/keyStore_test.go +++ b/keyStore_test.go @@ -16,16 +16,16 @@ func TestKeyStore(t *testing.T) { // Enum // Pros: Uses less space, probably less time as well // Cons: Requires import, key.A instead of "A", keybinds require an extended const block - setDown("Test") + SetDown("Test") assert.True(t, IsDown("Test")) - setUp("Test") + SetUp("Test") assert.False(t, IsDown("Test")) - setDown("Test") + SetDown("Test") time.Sleep(2 * time.Second) ok, d := IsHeld("Test") assert.True(t, ok) assert.True(t, d > 1950*time.Millisecond) - setUp("Test") + SetUp("Test") ok, d = IsHeld("Test") assert.False(t, ok) assert.True(t, d == 0) diff --git a/render/compositeR.go b/render/compositeR.go index ed47bbf1..b1595200 100644 --- a/render/compositeR.go +++ b/render/compositeR.go @@ -3,16 +3,19 @@ package render import ( "image" "image/draw" + "sync" "github.com/oakmound/oak/v2/alg/floatgeom" ) // A CompositeR is equivalent to a CompositeM for Renderables instead of -// Modifiables. CompositeRs can also be used as Draw Stack elements. +// Modifiables. CompositeRs also implements Stackable. type CompositeR struct { LayeredPoint - toPush []Renderable - rs []Renderable + toPush []Renderable + toUndraw []Renderable + rs []Renderable + predrawLock sync.Mutex DrawPolygon } @@ -21,6 +24,7 @@ func NewCompositeR(sl ...Renderable) *CompositeR { cs := new(CompositeR) cs.LayeredPoint = NewLayeredPoint(0, 0, 0) cs.toPush = make([]Renderable, 0) + cs.toUndraw = make([]Renderable, 0) cs.rs = sl return cs } @@ -101,22 +105,32 @@ func (cs *CompositeR) Get(i int) Renderable { // Add stages a renderable to be added to the Composite at the next PreDraw func (cs *CompositeR) Add(r Renderable, _ ...int) Renderable { + cs.predrawLock.Lock() cs.toPush = append(cs.toPush, r) + cs.predrawLock.Unlock() return r } // Replace updates a renderable in the CompositeR to the new Renderable -func (cs *CompositeR) Replace(r1, r2 Renderable, i int) { - cs.Add(r2, i) - r1.Undraw() +func (cs *CompositeR) Replace(old, new Renderable, i int) { + cs.predrawLock.Lock() + cs.toPush = append(cs.toPush, new) + cs.toUndraw = append(cs.toUndraw, old) + cs.predrawLock.Unlock() + } // PreDraw updates the CompositeR with the new renderables to add. // This helps keep consistency and mitigates the threat of unsafe operations. func (cs *CompositeR) PreDraw() { + cs.predrawLock.Lock() push := cs.toPush cs.toPush = []Renderable{} cs.rs = append(cs.rs, push...) + for _, r := range cs.toUndraw { + r.Undraw() + } + cs.predrawLock.Unlock() } // Copy returns a new composite with the same length slice of renderables but no actual renderables... diff --git a/render/drawHeap.go b/render/drawHeap.go index 0c07683e..66d75794 100644 --- a/render/drawHeap.go +++ b/render/drawHeap.go @@ -8,12 +8,13 @@ import ( ) // A RenderableHeap manages a set of renderables to be drawn in explicit layered -// order, using an internal heap to manage that order. +// order, using an internal heap to manage that order. It implements Stackable. type RenderableHeap struct { - rs []Renderable - toPush []Renderable - static bool - addLock sync.RWMutex + rs []Renderable + toPush []Renderable + toUndraw []Renderable + static bool + addLock sync.RWMutex DrawPolygon } @@ -27,6 +28,7 @@ func NewHeap(static bool) *RenderableHeap { rh := new(RenderableHeap) rh.rs = make([]Renderable, 0) rh.toPush = make([]Renderable, 0) + rh.toUndraw = make([]Renderable, 0) rh.static = static rh.addLock = sync.RWMutex{} return rh @@ -64,10 +66,13 @@ func (rh *RenderableHeap) Add(r Renderable, layers ...int) Renderable { return r } -//Replace adds a Renderable and removes an old one -func (rh *RenderableHeap) Replace(r1, r2 Renderable, layer int) { - rh.Add(r2, layer) - r1.Undraw() +// Replace adds a Renderable and removes an old one +func (rh *RenderableHeap) Replace(old, new Renderable, layer int) { + new.SetLayer(layer) + rh.addLock.Lock() + rh.toPush = append(rh.toPush, new) + rh.toUndraw = append(rh.toUndraw, old) + rh.addLock.Unlock() } // Satisfying the Heap interface @@ -105,6 +110,11 @@ func (rh *RenderableHeap) PreDraw() { heap.Push(rh, r) } } + for _, r := range rh.toUndraw { + if r != nil { + r.Undraw() + } + } rh.toPush = make([]Renderable, 0) rh.addLock.Unlock() } diff --git a/render/drawStack.go b/render/drawStack.go index 7d311902..06d7f8fe 100644 --- a/render/drawStack.go +++ b/render/drawStack.go @@ -87,6 +87,48 @@ func Draw(r Renderable, layers ...int) (Renderable, error) { return GlobalDrawStack.as[0].Add(r), nil } +// Replace replaces 'old' with 'new', undrawing old and drawing new simultaneously. +// It does no position manipulation of either renderable. It follows the same rules +// for layer arguments as Draw. +// +// Example: +// With a single stackable in the draw stack: +// render.Replace(r, r2, 0) +// replaces r with r2, setting r2's layer to 0. +// +// With multiple stackables in the draw stack: +// render.Replace(r, r2, 1, 5) +// replaces r (on stackable 1) with r2, setting r2's layer to 5. +func Replace(old, new Renderable, layers ...int) error { + if old == nil { + dlog.Error("Tried to replace a nil renderable") + return oakerr.NilInput{InputName: "old"} + } + if new == nil { + dlog.Error("Tried to insert a nil renderable") + return oakerr.NilInput{InputName: "new"} + } + if len(layers) == 0 { + return oakerr.InsufficientInputs{InputName: "layers"} + } + if len(GlobalDrawStack.as) == 1 { + GlobalDrawStack.as[0].Replace(old, new, layers[0]) + return nil + } + if len(layers) > 1 { + stackLayer := layers[0] + if stackLayer < 0 || stackLayer >= len(GlobalDrawStack.as) { + dlog.Error("Layer", stackLayer, "does not exist on global draw stack") + return oakerr.InvalidInput{InputName: "layers"} + } + GlobalDrawStack.as[stackLayer].Replace(old, new, layers[1]) + return nil + } + + GlobalDrawStack.as[0].Replace(old, new, layers[0]) + return nil +} + // Push appends a Stackable to the draw stack during the next PreDraw. func (ds *DrawStack) Push(a Stackable) { ds.toPush = append(ds.toPush, a) diff --git a/render/fps.go b/render/fps.go index 8535feb7..9eefc373 100644 --- a/render/fps.go +++ b/render/fps.go @@ -15,7 +15,8 @@ const ( FpsSmoothing = .25 ) -// DrawFPS is a draw stack element that will draw the fps onto the screen +// DrawFPS is a Stackable that will draw the fps onto the screen when a part of the +// draw stack. type DrawFPS struct { fps int lastTime time.Time diff --git a/render/logicfps.go b/render/logicfps.go index 75beaafe..7e23aae2 100644 --- a/render/logicfps.go +++ b/render/logicfps.go @@ -9,7 +9,8 @@ import ( "github.com/oakmound/oak/v2/timing" ) -// LogicFPS is a draw stack element that will draw the logical fps onto the screen +// LogicFPS is a Stackable that will draw the logical fps onto the screen when a part +// of the draw stack. type LogicFPS struct { event.CID fps int diff --git a/scene/map.go b/scene/map.go index d8732f88..88ce30ea 100644 --- a/scene/map.go +++ b/scene/map.go @@ -46,12 +46,26 @@ func (m *Map) Add(name string, start Start, loop Loop, end End) error { // AddScene takes a scene struct, checks that its assigned name does not // conflict with an existing name in the map, and then adds it to the map. -// If a conflict occurs, the scene will not be overwritten. Todo: this could -// change, with a function argument specifying whether or not the scene should +// If a conflict occurs, the scene will not be overwritten. +// Checks if the Scene's start is nil, sets to noop if so. +// Checks if the Scene's loop is nil, sets to infinite if so. +// Checks if the Scene's end is nil, sets to loop to this scene if so. +// Todo: this could change, with a function argument specifying whether or not the scene should // overwrite. func (m *Map) AddScene(name string, s Scene) error { dlog.Info("[oak]-------- Adding", name) var err error + + if s.Start == nil { + s.Start = func(prevScene string, data interface{}) {} + } + if s.Loop == nil { + s.Loop = func() bool { return true } + } + if s.End == nil { + s.End = GoTo(name) + } + m.lock.Lock() if _, ok := m.scenes[name]; ok { err = oakerr.ExistingElement{ diff --git a/scene/map_test.go b/scene/map_test.go index ac5507ff..844d4a16 100644 --- a/scene/map_test.go +++ b/scene/map_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMap(t *testing.T) { @@ -28,3 +29,18 @@ func TestTransition(t *testing.T) { assert.False(t, zoomFn(nil, 11)) assert.True(t, zoomFn(image.NewRGBA(image.Rect(0, 0, 50, 50)), 2)) } + +func TestAddScene(t *testing.T) { + m := NewMap() + _, ok := m.Get("badScene") + assert.False(t, ok) + + m.AddScene("test1", Scene{}) + test1, ok := m.Get("test1") + require.True(t, ok) + + require.True(t, test1.Loop()) + eStr, _ := test1.End() + assert.Equal(t, "test1", eStr) + test1.Start("test", nil) +}